React+threejsで3Dモデルのビューアーを作る!!

概要

はじめまして!テックセンター飯塚の株式会社オプティムのアルバイトスタッフの下前です。普段は次期ストアのタスクを行っています。
この記事はC3との合同イベントのハンズオン資料です。
three.js, react-three-fiber, react-three-dreiなどのjsライブラリを用いて3Dモデルを表示るビュアーを作成していきます。

C3(Composite Computer Club)は九州工業大学情報工学部のコンピューターでモノづくりをするサークルです。情報工学部のキャンパスの目の前に OPTiMのオフィスがあるということもあり、私も含めた何人かのC3メンバーがアルバイトスタッフとして勤務しています。 そこで九州工業大学前アルバイトスタッフとC3との共同でハンズオンイベントを行いました。

イベントの他の記事はこちら

tech-blog.optim.co.jp tech-blog.optim.co.jp

前提

↓の記事でやっているようにLiDARセンサーで取得したlas形式の点群データをply形式に変換し、さらにobj形式に変換した3Dモデルが手元にある前提で行います。一般的なobj形式の3Dモデルでも問題ありません。

tech-blog.optim.co.jp

  • OSはWindowsを想定
  • 事前準備で以下が出来るようになっていること
  • Dockerが使える
  • VSCodeが使える
  • GitHubからクローンができる

※ Docker Desktop on windowsでの環境ではホットリロードが非常に遅いです。wsl(Ubuntuなど)やmac osの環境がある場合はそちらを使うことをお勧めします。上記は環境はイベント参加者がwindowsユーザーが多いことやwslセットアップが出来ていないことを考慮してのものですので、特別な理由がない限りはwsl(Ubuntuなど)やmac osでやることをお勧めします。

Dockerについて

一言で言うとオーバーヘッドの少ない(軽い)仮想環境です。今回これを使うのは開発環境を作るのにさまざまな作業が必要なため、それを回避するのに使用しています。ソフトウェアの開発ではよく用いられています。

コンテナは仮想化技術の1つの形態と言われていますが、実際には一般的な仮想マシンとはアーキテクチャーが異なります。まずコンテナと仮想マシンの違いについて具体的に触れていきましょう。

コンテナの特徴は、ハードウェア上に存在するOSが1つのみであり、その上で複数のコンテナが稼働している仕組みになっていることです。コンテナアプリはOSから分離しているため、OSに起因するオーバーヘッドがかからずにリソースを有効活用できます。IPアドレスは自動設定で、外部とはNAT(Network Address Translation)で接続します。

一方で、一般的な仮想マシンではハードウェアのリソースを、ハイパーバイザーを介してLinuxやWindowsなど複数のOS環境に分けて、それぞれで仮想マシンが稼働します。仮想マシンごとにOSを用意するため、アプリに合わせてOSを選択できます。また、物理と同様に、IP管理も実施できます。仮想マシンは、物理環境で実装可能な機能はほぼすべて実現できるという意味で、信頼の厚い手法と言えるでしょう。実際に、現在のエンタープライズアプリケーションの稼働環境として、最も広く使われています。

参考:仮想マシンとコンテナの違いとは?~比較から見えるそれぞれの特徴~ | VMware Cloud Frontier by Networld


参照:Dockerとは一体何なんだ?【初心者向け】

参照:Docker初心者がDockerfile,Image,Containerの関係を感覚的に理解する

詳細は↓

www.kagoya.jp

手順

配布リポジトリのアプリを起動

↓今回使うリポジトリ
github.com

このリポジトリは以下の2つのブランチが存在します。完成版を見たい方はdemoブランチにアクセスしてください。
main:ハンズオン用のテンプレートコード
demo:完成版

  • GitHubからソースコードをローカルにクローン(ダウンロード)する
git clone https://github.com/optim-corp/techblog-c3optim-event-react-threejs-sample.git
  • ソースコードをVSCodeで開く
code techblog-c3optim-event-react-threejs-sample
  • VSCodeのターミナルを開く 以下のように上部のメニューからTerminalを選択し、New Tarminalを選択する
    そうすると以下のようにターミナルが開く

  • Dockerのimageを作成 開いたターミナルで以下のコマンドを実行する

docker compose build

正常に終了すると以下のようになる

  • Dockerのコンテナを起動 ビルドが完了したら以下のコマンドを実行する
docker compose up -d

実行すると以下のようになる

  • ブラウザで動作確認 http://localhost:3000 にアクセスして、以下のようなページが表示されればOK

VSCodeで開発環境を作る

  • VSCodeの拡張機能を開く 以下の画像のように左のメニューバーから拡張を選択し、開く

  • ワークスペース推奨の拡張機能をすべて入れる 以下の画像のように検索ボックスに@recommendedを入れるとワークスペース推奨が表示されるので、表示されたすべての拡張をインストールする

インストールした後にReload Requiredと表示されたら、そのボタンをおして画面の再読み込みを行う。もし画面の再読み込みをしないと拡張機能が動作しない。

  • DevContainerでDockerコンテナに入る 画像のようにVSCodeの左のメニューでRemote Explorerを選択し、表示された画面の上部にあるプルダウンでDev Containersを選択する。

表示された中にtechblog-c3optim-....があるので、それを選択すると新しいウィンドウが表示される。接続までに時間がかかるので少し待つ。

  • アプリのディレクトリを開く 以下の画像のようにVSCodeの上部のメニューでFileを選択し、Open Folderを選択する

そうすると以下のようなモーダルが表示されるので、/threejs/を入れOKを押す。

  • 以下のようになればOK

アプリの説明

概要

3Dモデルを表示し、ボタンで表示を切り替えれるようなビューアー

環境

node: v20.2.0
言語: TypeScript
パッケージ管理: pnpm
ビルドツール: vite
nodeバージョン管理: volta
コードフォーマッター: prettier
使用パッケージ:

"@react-three/drei": "^9.74.1",  
"@react-three/fiber": "^8.13.0",  
"leva": "^0.9.34",  
"react": "^18.2.0",  
"react-dom": "^18.2.0",  
"react-icons": "^4.9.0",  
"three": "^0.152.2",  
"three-stdlib": "^2.23.8",  
"typescript": "^4.9.5"

Reactについて

ReactはWeb開発で用いるJSのフレームワークです。ウェブに必要な機能などを簡単に実装できるようになるため、多くのウェブ開発で使用されています。React以外にもWeb制作に使うJSライブラリが多くあるので、興味があれば確認してください。

参考
www.kagoya.jp ja.legacy.reactjs.org

ディレクトリ説明

/
├ .vscode(ワークスペースのVSCodeの設定)
│ └ ...
├ build(ビルドしたファイルを格納)
│ └...
├ node_modules(パッケージコード)
│ └ ...
├ public(アセットなど)
│ └ ...
├ src(Reactのソースコード)
│ ├ App.css
│ ├ App.tsx
│ ├ Fotter.tsx
│ ├ Fotter.css
│ ├ index.css
│ ├ index.tsx
│ ├ ModelView.tsx
│ └ vite-app-env.d.ts
├ .gitignore(github管理を無視する設定)
├ compose.yml(docker composeの設定ファイル)
├ Dockerfile
├ index.html
├ package.json(パッケージ管理)
├ pnpm-lock.yaml(インストール済みのパッケージ情報)
├ README.md(ドキュメント)
├ tsconfig.json(TSの設定)
└ vite.config.ts(viteの設定)

アプリ作成

目次
1. 3Dモデルのダウンロード
2. 3Dモデルを表示する
3. 光を追加する
4. 影をつける
5. 背景をつける
6. ローディング表示を追加する
7. xyz軸の表示を消す
8. ボタンで表示するモデルを切り替えれるようにする

3Dモデルのダウンロード

今回使用する以下の3Dモデルをダウンロードし、プロジェクトのpublicフォルダ内に入れてください。

↓今回使用する3Dモデル
https://public.compositecomputer.club/c3optim/model1.obj
https://public.compositecomputer.club/c3optim/model2.obj
https://public.compositecomputer.club/c3optim/model3.obj

3Dモデルを表示する

  • ModelView.tsxファイルを作成する
    画像のようにsrcフォルダを一度クリックし、赤枠のファイル追加ボタンを押す。

    入力が求められるので、ModelView.tsxと記入する

  • モデルビューアーを定義する
    ModelView.tsxファイルを開き、以下のコードをコピペする。

// パッケージのインポート
import { useRef, Suspense } from "react";
import { useLoader } from "@react-three/fiber";
import { Mesh } from "three";
import { OrbitControls as OrbitControlImpl } from "three-stdlib";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import { OrbitControls, Preload, PerspectiveCamera } from "@react-three/drei";

// ModelViewのpropsの型定義
type ModelViewProps = {
  model_url: string;
  key: number;
};

// モデルローダー
const Model = (args: any) => {
  const model = useLoader(OBJLoader, args.model_url as string);
  model.rotation.x = -1.5;

  return (
    <primitive
      object={model}
      ref={args.mesh as Mesh}
      scale={[0.1, 0.1, 0.1]}
      {...args}
    />
  );
};

function ModelView(props: ModelViewProps) {
  // コントローラー用Refの定義
  const controlRef = useRef<OrbitControlImpl>({} as OrbitControlImpl);
  //   メッシュ定義
  const mesh = useRef({} as Mesh);

  return (
    <>
      {/* ローディングが終わるまでは何も表示しない */}
      <Suspense fallback={null}>
        {/* 上記で定義したモデルローダーで表示される3Dモデル */}
        <Model mesh={mesh} model_url={props.model_url} position={[0, 0, 0]} />
        <Preload all />
        {/* 遠近カメラ */}
        <PerspectiveCamera
          makeDefault
          args={[35, window.innerWidth / window.innerHeight, 0.1, 2000]}
          position={[-5, 4, 5]}
        />
      </Suspense>
      {/* マウスでの操作を可能にする */}
      <OrbitControls makeDefault ref={controlRef} />
      {/* xyz軸を表示 */}
      <axesHelper />
    </>
  );
}
// ModelViewコンポーネントとして出力
export default ModelView;
  • 表示する
    App.tsxを開き、以下のように変更する
// パッケージのインポート
import { useState } from "react";
import { Canvas } from "@react-three/fiber";

// コンポーネントのインポート
import Footer from "./Footer";
// ↓ここを追加
import ModelView from "./ModelView";

// cssのインポート
import "./App.css";

function App() {
  return (
    <div className="App">
       {/* ↓ここを追加 */}
      <Canvas className="canvas">
        <ModelView model_url="/model1.obj" key={0} />
      </Canvas>
      <Footer />
    </div>
  );
}

export default App;

Canvasはreact-three-fiberのコンポーネントでthreejsで描画する領域を定義するためのコンポーネントです。

先ほど定義したModelViewコンポーネントをimport ModelView from "./ModelView";でインポートし、

<ModelView model_url="/model1.obj" key={0} />

propsとしてmodel_urlに3Dモデルのパス、keyに番号を渡している。

WebGLでは物体、光源、カメラが最低限必要になってくる。WebGLにおいて描画されるのはカメラから見た構図となる。

参考 ics.media

  • ブラウザで確認
    画像のように表示されるはずです。黒い塊が表示されていますが、これが3Dモデルです。

    黒い塊と座標軸が表示されているだけでとても見やすいとは言えないと思います。
    3Dモデルが黒く表示されているのは光源の設定をしていないため、暗闇の中にある物体をカメラで見ていることになり、何も光が反射してこないので黒く見えているのです。私たちが目で見ているものはすべて光源からの光や物体からの反射光によるものですが、threejs(OpenGL)でも同じ原理になっています。

光を追加する

物体の本来の色を見たいので、光を追加していきます。

  • 環境光を追加する
    ModelView.tsxambientLightを以下のように追加する。
    <>
      <Suspense fallback={null}>
        <Model mesh={mesh} model_url={props.model_url} position={[0, 0, 0]} />
        <Preload all />
        <PerspectiveCamera
          makeDefault
          args={[35, window.innerWidth / window.innerHeight, 0.1, 2000]}
          position={[-5, 4, 5]}
        />
        {/* ↓ここを追加 */}
        <ambientLight color="white" intensity={0.5} />
        {/* ここまで */}
      </Suspense>
      <OrbitControls makeDefault ref={controlRef} />
      <axesHelper />
    </>

ambient lightは環境光を意味します。環境光とは現実世界における自然光の乱反射を再現する光のことです。intensityは明るさを調整するパラメータです。
wgld.org

  • 平行光源を追加する
    ModelView.tsxdirectionalLightを以下のように追加する。
    <>
      <Suspense fallback={null}>
        <Model mesh={mesh} model_url={props.model_url} position={[0, 0, 0]} />
        <Preload all />
        <PerspectiveCamera
          makeDefault
          args={[35, window.innerWidth / window.innerHeight, 0.1, 2000]}
          position={[-5, 4, 5]}
        />
        <ambientLight color="white" intensity={0.5} />
        {/* ↓ここを追加 */}
        <directionalLight color="white" intensity={0.8} position={[1, 5, 5]} />
        {/* ここまで */}
      </Suspense>
      <OrbitControls makeDefault ref={controlRef} />
      <axesHelper />
    </>

directional lightは平行光源を意味します。平行光源とは無限遠から物体を照らしている光源のことです。例えば、太陽は平行光源です。
wgld.org

光源の設定ができたので以下のようにモデルの本来の色が見えるようになります。

モデルを中央に持ってくる

現在このように表示されていると思います。モデルの端がxyz軸の原点にあると思います。マウスなどでモデルを動かすと原点を中心に支店が変わると思います。この状態だと違和感があるので、モデルの中心を原点にしたいと思います。

以下のようにCenterコンポーネントを追加するとモデルの中心が原点に配置されます

import { useRef, Suspense } from "react";
import { useLoader } from "@react-three/fiber";
import { Mesh } from "three";
import { OrbitControls as OrbitControlImpl } from "three-stdlib";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
// ↓ここを変更
import { Center, OrbitControls, Preload, PerspectiveCamera } from "@react-three/drei";

...

    <>
      <Suspense fallback={null}>
        {/* ↓ここを変更 */}
        <Center>
          <Model mesh={mesh} model_url={props.model_url} position={[0, 0, 0]} />
        </Center>
        {/* ここまで */}
        <Preload all />
        <PerspectiveCamera
          makeDefault
          args={[35, window.innerWidth / window.innerHeight, 0.1, 2000]}
          position={[-5, 4, 5]}
        />
        <ambientLight color="white" intensity={0.5} />
        <directionalLight color="white" intensity={0.8} position={[1, 5, 5]} />
      </Suspense>
      <OrbitControls makeDefault ref={controlRef} />
      <axesHelper />
    </>

これで3Dモデルの中心が原点になるように描画されたと思います。
このコンポーネントはいい感じに計算してポジションを決めてくれるので、簡単にモデルを中心に持ってくることができます。

影をつける

以下のようにContactShadowsコンポーネントを追加する。

import { useRef, Suspense } from "react";
import { useLoader } from "@react-three/fiber";
import { Mesh } from "three";
import { OrbitControls as OrbitControlImpl } from "three-stdlib";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
// ↓ここを変更
import {
  Center,
  ContactShadows,
  OrbitControls,
  Preload,
  PerspectiveCamera
} from "@react-three/drei";

...

    <>
      <Suspense fallback={null}>
        <Center>
          <Model mesh={mesh} model_url={props.model_url} position={[0, 0, 0]} />
        </Center>
        <Preload all />
        <PerspectiveCamera
          makeDefault
          args={[35, window.innerWidth / window.innerHeight, 0.1, 2000]}
          position={[-5, 4, 5]}
        />
        <ambientLight color="white" intensity={0.5} />
        <directionalLight color="white" intensity={0.8} position={[1, 5, 5]} />
        {/* ↓ここを追加 */}
        <ContactShadows
          opacity={0.7}
          scale={15}
          blur={2.2}
          far={20}
          resolution={256}
          position={[0, -0.4, 0]}
        />
        {/* ここまで */}
      </Suspense>
      <OrbitControls makeDefault ref={controlRef} />
      <axesHelper />
    </>

追加すると以下のような影が表示されます。

背景をつける

import { useRef, Suspense } from "react";
import { useLoader } from "@react-three/fiber";
import { Mesh } from "three";
import { OrbitControls as OrbitControlImpl } from "three-stdlib";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
// ↓ここを変更
import {
  Backdrop,
  Center,
  ContactShadows,
  OrbitControls,
  Preload,
  PerspectiveCamera,
} from "@react-three/drei";

...

    <>
      <Suspense fallback={null}>
         {/* ↓ここを追加 */}
        <Backdrop
          receiveShadow
          scale={[20, 5, 5]}
          floor={1}
          position={[0, -0.5, -2]}
        >
          <meshPhysicalMaterial roughness={1} color="#a0d8ef" />
        </Backdrop>
        {/* ここまで */}
        <Center>
          <Model mesh={mesh} model_url={props.model_url} position={[0, 0, 0]} />
        </Center>
        <Preload all />
        <PerspectiveCamera
          makeDefault
          args={[35, window.innerWidth / window.innerHeight, 0.1, 2000]}
          position={[-5, 4, 5]}
        />
        <ambientLight color="white" intensity={0.5} />
        <directionalLight color="white" intensity={0.8} position={[1, 5, 5]} />
        <ContactShadows
          opacity={0.7}
          scale={15}
          blur={2.2}
          far={20}
          frames={1}
          resolution={256}
          position={[0, -0.4, 0]}
        />
      </Suspense>
      <OrbitControls makeDefault ref={controlRef} />
      <axesHelper />
    </>

これによって以下のように撮影スタジオのような背景ができます。

ローディング表示を追加する

3Dモデルの読み込み・描画には時間がかかるため、ページ読み込み時には何も表示されません。そこで、ローディング中に何かしらの要素を表示したいと思います。
以下のようにコードを追加します。

...
  Backdrop,
  Center,
  ContactShadows,
  OrbitControls,
  Preload,
  PerspectiveCamera,
  // ↓ここを追加
  useProgress,
  Html,
  // ここまで
} from "@react-three/drei";

// ↓ここを追加
// ローディング要素
function Loading() {
  const { progress } = useProgress();
  console.log(progress + "% loading...");
  return <Html className="loading"></Html>;
}
// ここまで

// モデルローダー
const Model = (args: any) => {

...

    <>
      {/* ↓ここを変更 */}
      <Suspense fallback={<Loading />}>
        <Backdrop
          receiveShadow
          scale={[20, 5, 5]}
          floor={1}

useProgress()はreact-three-dreiのメソッドで、読み込みのパーセンテージを返り値として取得できます。Htmlコンポーネントは読み込み中に表示する要素を記述するためのものです。

今回はcssアニメーションで立方体を動かしています。cssはすでに記述済みなので、classNameloadingクラスを付与するだけでよい。
これによって、3Dモデルの読み込み中にアニメーションを表示できる。

xyz軸の表示を消す

座標軸の表示は邪魔なので、この辺で消しておきます。<axesHelper />の記述を削除すると表示されなくなります。

ボタンで表示するモデルを切り替えれるようにする

ここまででモデルのビューアーは完成しました。ここでは、ボタンで表示する3Dモデルの表示を切り替えれるようにしたいと思います。

App.tsxを開いて、以下のようにコードを修正します。

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

// コンポーネントのインポート
import Footer from "./Footer";
import ModelView from "./ModelView";

import "./App.css";

function App() {
  // ↓ここから追加
  // 表示する3Dモデルのインデックスの状態
  const [selectedModel, setSelectedModel] = useState<number>(0);
  // 3Dモデルのパスを保持する変数
  const models: string[] = ["/model1.obj", "/model2.obj", "/model3.obj"];
  // ここまで

  return (
    <div className="App">
      {/* ↓ここから追加 */}
      {/* 3Dモデル切り替え用ボタン */}
      <div className="model_switch_button">
        {/* 繰り返し処理 */}
        {models.map((_, index) => {
          return (
            <div
              className={`${index === selectedModel ? "selected" : ""}`}
              key={index}
              // クリックイベントの処理
              onClick={() => {
                setSelectedModel(index);
              }}
            >
              {index + 1}
            </div>
          );
        })}
      </div>
      {/* ここまで */}
      <Canvas className="canvas">
        <ModelView model_url={models[selectedModel]} key={0} />
      </Canvas>
      <Footer />
    </div>
  );
}

export default App;

useStateで状態管理用の変数とメソッドを定義します。selectedModelが変数で、setSelectedModelselectedModelに値を格納するためのメソッドです。

ボタンを3Dモデルの数(modelsの配列の長さ)だけ表示したいので、map関数を使って繰り返し処理を行います。map関数はJavaScript/TypeScriptの組み込み関数でfor文のような処理を行えます。この繰り返し処理の中でHTML要素をreturnすることで、3Dモデルの数だけボタンを表示することができます。

次にクリックしたときにモデルを切り替える方法ですが、onClickという属性がreactにあり、これはonClickを記述したHTML要素がクリックされたときにそのイベントをキャッチして、指定された処理を行うことができます。

onClick={() => {
  setSelectedModel(index);
}}

今回の場合はselectedModelmodelsindexを格納し、

<ModelView model_url={models[selectedModel]} key={selectedModel} />

ModelViewコンポーネントのpropsのmodel_urlに選択されているボタンに対応する3Dモデルのパスを渡している。

これによって左上にボタンが表示されたと思います。選択中以外のボタンを選択すると他の3Dモデルが読み込まれて表示されます。

最終的な実装

ModelView.tsx

// パッケージのインポート
import { useRef, Suspense } from "react";
import { useLoader } from "@react-three/fiber";
import { Mesh } from "three";
import { OrbitControls as OrbitControlImpl } from "three-stdlib";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import {
  Backdrop,
  Center,
  ContactShadows,
  OrbitControls,
  Preload,
  PerspectiveCamera,
  useProgress,
  Html,
} from "@react-three/drei";

// ModelViewのpropsの型定義
type ModelViewProps = {
  model_url: string;
  key: number;
};

// ローディング要素
function Loading() {
  const { progress } = useProgress();
  console.log(progress + "% loading...");
  return <Html className="loading"></Html>;
}

// モデルローダー
const Model = (args: any) => {
  const model = useLoader(OBJLoader, args.model_url as string);
  model.rotation.x = -1.5;

  return (
    <primitive
      object={model}
      ref={args.mesh as Mesh}
      scale={[0.1, 0.1, 0.1]}
      {...args}
    />
  );
};

function ModelView(props: ModelViewProps) {
  // コントローラー用Refの定義
  const controlRef = useRef<OrbitControlImpl>({} as OrbitControlImpl);
  // メッシュ定義
  const mesh = useRef({} as Mesh);

  return (
    <>
      {/* ローディングが終わるまでは何も表示しない */}
      <Suspense fallback={<Loading />}>
        <Backdrop
          receiveShadow
          scale={[20, 5, 5]}
          floor={1}
          position={[0, -0.5, -2]}
        >
          <meshPhysicalMaterial roughness={1} color="#a0d8ef" />
        </Backdrop>
        <Center>
          {/* 上記で定義したモデルローダーで表示される3Dモデル */}
          <Model mesh={mesh} model_url={props.model_url} position={[0, 0, 0]} />
        </Center>
        <Preload all />
        {/* 遠近カメラ */}
        <PerspectiveCamera
          makeDefault
          args={[35, window.innerWidth / window.innerHeight, 0.1, 2000]}
          position={[-5, 4, 5]}
        />
        <ambientLight color="white" intensity={0.5} />
        <directionalLight color="white" intensity={0.8} position={[1, 5, 5]} />
        <ContactShadows
          opacity={0.7}
          scale={15}
          blur={2.2}
          far={20}
          frames={1}
          resolution={256}
          position={[0, -0.4, 0]}
        />
      </Suspense>
      {/* マウスでの操作を可能にする */}
      <OrbitControls makeDefault ref={controlRef} />
    </>
  );
}
// ModelViewコンポーネントとして出力
export default ModelView;

App.tsx

// パッケージのインポート
import { useState } from "react";
import { Canvas } from "@react-three/fiber";

// コンポーネントのインポート
import Footer from "./Footer";
import ModelView from "./ModelView";

// caaのインポート
import "./App.css";

function App() {
  // 表示する3Dモデルのインデックスの状態
  const [selectedModel, setSelectedModel] = useState<number>(0);
  // 3Dモデルのパスを保持する変数
  const models: string[] = ["/model1.obj", "/model2.obj", "/model3.obj"];

  return (
    <div className="App">
      {/* 3Dモデル切り替え用ボタン */}
      <div className="model_switch_button">
        {/* 繰り返し処理 */}
        {models.map((_, index) => {
          return (
            <div
              className={`${index === selectedModel ? "selected" : ""}`}
              key={index}
              // クリックイベントの処理
              onClick={() => {
                setSelectedModel(index);
              }}
            >
              {index + 1}
            </div>
          );
        })}
      </div>
      <Canvas className="canvas">
        <ModelView model_url={models[selectedModel]} key={selectedModel} />
      </Canvas>
      <Footer />
    </div>
  );
}

export default App;

デモ

最後に

コンテナを起動させた状態だと非常にPCが遅くなるので、コンテナを落とします。
以下のコマンドを起動時と同じディレクトリの中で実行します。

docker compose down -v

再度以下を実行すればアプリを動かせます

docker compose up -d

以上でハンズオンは終了です!

OPTiMではエンジニアを随時募集しております。ご興味のある方はこちらをご覧ください。

www.optim.co.jp

参考

HTML & CSS

HTML&CSS入門:イチからWebデザインを習得する講座 はじめてのWebデザイン『HTML・CSS』入門 | chot.design

REACT

チュートリアル:React の導入 – React React入門 ~基礎編~

Docker

Dockerとは一体何なんだ?【初心者向け】 - RAKUS Developers Blog | ラクス エンジニアブログ Docker初心者がDockerfile,Image,Containerの関係を感覚的に理解する | しぃたけ LOG 【入門】Dockerとは?概要やメリット、インストール方法をわかりやすく解説 - カゴヤのサーバー研究室

コンピュータグラフィックス

コンピューターグラフィックスはどうやって作られるのか?CGの基礎原理を完全に理解する! | 3DCG school http://www.ieice-hbkb.org/files/02/02gun_03hen_01.pdf

threejs

Three.js入門サイト - ICS MEDIA three.js docs TOPIC 12|Three.jsで活用する[2/2]|ReactでThree.jsを扱う | How To Use | PLATEAU [プラトー] 最新版で学ぶThree.js入門 - 手軽にWebGLを扱える3Dライブラリ - ICS MEDIA 最新版で学ぶThree.js入門 - 手軽にWebGLを扱える3Dライブラリ - ICS MEDIA

react-three/fiber

React Three Fiber Documentation 驚くほど簡単に3Dシーンを構築!React Three Fiberを使ってみた | 株式会社LIG(リグ)|DX支援・システム開発・Web制作 React Three Fiber + TypeScript: 3次元空間で立方体を回してみる - Qiita

react-three/drei

@storybook/cli - Storybook react-three-fiber/dreiで3Dモデルビューを手軽に実装する