Next.js(React)でThree.jsを使用してサメの3Dモデルを表示してみる

# What is Three.js?
![sharkgood.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706631/c1560d24-9f68-f43c-b6ca-c0311ea10741.gif)

ブラウザ上に3Dオブジェクトを表示することができるJavaScriptの3Dライブラリです。

紹介されているサイトはどれもめちゃくちゃかっこいいので、みてみるのおすすめです！

https://threejs.org/

今回はsketchfabという3DモデルをダウンロードできるサイトからglTF形式の3Dアニメーションモデルを画面に描画してみました。

[ハンマーヘッドシャークの素材](https://sketchfab.com/3d-models/model-73a-great-hammerhead-shark-77d52f2b0e084fe7bcefbc86b920f080)

⬇︎デプロイ先

https://fish3d.vercel.app/

⬇︎githubURL

https://github.com/developerhost/fish3d

# next.jsのプロジェクトを作成

```terminal
npx create-next-app
✔ Would you like to use TypeScript with this project? … Yes
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … Yes
✔ Would you like to use `src/` directory with this project? … Yes
✔ Use App Router (recommended)? … No
✔ Would you like to customize the default import alias? … No
```

今回はApp Routerは使用していません。

npm run devでサーバーを起動するとページが表示されます。

# 必要なライブラリをインストール
```terminal
npm install three three-stdlib
npm install react-three/drei --save
npm install three @react-three/fiber --save
npm install --save-dev @types/three
```

型定義もinstallし、今回はreact-threeを使用するので、それもinstallします。

# 3D Modelをインストール
3D Modelは[sketchfab](https://sketchfab.com/feed)からダウンロードしました。
フォーマットはglTFを選択しましょう。

表示されているモデルはアイコンをみるとダウンロード可能か、有料か、アニメーションがあるのかを判断できます。

今回は[ハンマーヘッドシャークの素材](https://sketchfab.com/3d-models/model-73a-great-hammerhead-shark-77d52f2b0e084fe7bcefbc86b920f080)をダウンロードしました。

# ディレクトリ構成
- 📂 **project-root**
  - 📂 **.next/** (ビルド後に生成される)
  - 📂 **node_modules/** (依存関係のパッケージ)
  - 📂 **public/** (静的ファイル)
    - 🖼️ favicon.ico
    - 🖼️ vercel.svg
    - ここにglTFファイルのzipを解凍したものを入れる
    - img
        - water.jpegを置く
  - 📂 **src/** (ソースファイル)
    - 📂 **components/**
      - 📄 GeoOcean.tsx
    - 📂 **pages/**
      - 📄 _app.js
      - 📄 _document.js
      - 📄 index.js
    - 📂 **styles/**
      - 📄 globals.css
  - 📄 **.gitignore**
  - 📄 **next.config.js** (オプション)
  - 📄 **package.json**
  - 📄 **README.md**

publicに3Dモデルとテクスチャのjpegを入れます。
テクスチャに関しては後述します。

# 表示ページを作成
```tsx:index.tsx
import Image from 'next/image'
import { Inter } from 'next/font/google'
import * as THREE from 'three'
import { useEffect } from 'react'
import GeoOcean from '@/components/GeoOcean'

const inter = Inter({ subsets: ['latin'] })

export default function Home() {
  // console.log(THREE)
  return (
    <main
      className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
    >
      <div>
        <GeoOcean />
      </div>
    </main>
  )
}

```

GeoOceanコンポーネントに実際の描画の処理を書きます

# 3Dモデル表示コンポーネント
## 必要なライブラリをimport

```tsx:GeoOcean.tsx
import * as THREE from 'three'
import React, { Suspense, useRef, useMemo, useEffect } from 'react'
import { Canvas, extend, useThree, useLoader, useFrame, Object3DNode } from '@react-three/fiber'
import { OrbitControls, Sky } from '@react-three/drei'
import { GLTFLoader, Water } from 'three-stdlib'
import { AnimationMixer } from 'three';
```

## 型定義や型の拡張を行う
```tsx:GeoOcean.tsx
type ModelProps = {
  url: string;
};

extend({ Water })

declare global {
  namespace JSX {
    interface IntrinsicElements {
      water: Object3DNode<Water, typeof Water>
    }
  }
}
```

今回オブジェクトだけでなく、海や太陽の背景も表示しました。
その際にwaterを表示するためにJSXの型の拡張が必要でした。

## 海を表示する関数定義

water.jpegはここから取ってきてました

https://codesandbox.io/s/1b40u

```tsx:GeoOcean.tsx
function Ocean() {
  const ref = useRef<any>();
  const gl = useThree((state) => state.gl)
  const waterNormals = useLoader(THREE.TextureLoader, '/img/water.jpeg')
  waterNormals.wrapS = waterNormals.wrapT = THREE.RepeatWrapping
  const geom = useMemo(() => new THREE.PlaneGeometry(10000, 10000), [])
  const config = useMemo(
    () => ({
      textureWidth: 512,
      textureHeight: 512,
      waterNormals,
      sunDirection: new THREE.Vector3(),
      sunColor: 0xffffff,
      waterColor: 0x001e0f,
      distortionScale: 3.7,
      fog: false
      // format: gl.encoding
    }),
    [waterNormals]
  )
  useFrame((state, delta) => (ref.current.material.uniforms.time.value += delta))
  return <water ref={ref} args={[geom, config]} rotation-x={-Math.PI / 2} />
}
```

format: gl.encodingはなぜかエラーになるので、コメントアウトしてます。
有識者がいればここの仕様を教えてください。

## モデルを表示する関数を定義
```tsx:GeoOcean.tsx
function Model({ url }: ModelProps) {
  const ref = useRef<any>()
  const mixerRef = useRef<AnimationMixer | null>(null);
  useFrame((state, delta) => {
    // ref.current.position.y = 10 + Math.sin(state.clock.elapsedTime) * 20
    ref.current.position.y = 10
    // ref.current.rotation.y += delta
    ref.current.rotation.x = -0.2
    ref.current.rotation.y = 6.2
    ref.current.rotation.z = 0

    if (mixerRef.current) {
      mixerRef.current.update(delta);
    }
  })
  const { scene, animations } = useLoader(GLTFLoader, url)

  useEffect(() => {
    if (animations && animations.length > 0) {
      mixerRef.current = new AnimationMixer(ref.current);
      animations.forEach((clip: THREE.AnimationClip) => {
        mixerRef.current?.clipAction(clip).play();
      });
    }

    return () => {
      if (mixerRef.current) {
        mixerRef.current.stopAllAction();
      }
    };
  }, [animations])
  return (
    <primitive ref={ref} object={scene} dispose={null} scale={[8, 8, 8]} position={[0, 0, 0]} />
  )
}
```

useLoader(GLTFLoader, url)
これでモデルを読み込んでいます。

AnimationMixerを使用して3Dモデルを動かしています。

<primitive ref={ref} object={scene} dispose={null} scale={[8, 8, 8]} position={[0, 0, 0]} />

ここでモデルを表示してます。
3Dモデルをそのまま表示すると小さすぎたのでscaleを8倍にしてます。
また、positionを設定することでオブジェクトがカメラの中心にくるようになってます。

# コンポーネントを表示する部分
```tsx:GeoOcean.tsx
export default function GeoOcean() {
  return (
    // positionはカメラの位置
    <Canvas camera={{ position: [-20, 5, 20], fov: 55, near: 1, far: 20000 }} style={{width: '1000px', height: '1000px'}} gl={{ antialias: true, alpha: false }}>
      <pointLight position={[100, 100, 100]} />
      <pointLight position={[-100, -100, -100]} />
      <ambientLight intensity={0.5} />
      <Suspense fallback={null}>
        <Ocean />
        <Model url="/3d-model/model_great_hammerhead_shark/scene.gltf" />
      </Suspense>
      <Sky sunPosition={[500, 150, -1000]} turbidity={0.1} />
      <OrbitControls target={new THREE.Vector3(0, 8, 0)} />
    </Canvas>
  )
}
```

[OrbitControls](https://threejs.org/docs/#examples/en/controls/OrbitControls)
- カメラの位置をコントロールできる。マウスでカメラの位置を変更したり、zoom操作も可能です。

# 今後
今後このアプリはサメ図鑑としてさらにサメの種類を増やしていきます。

<Model url="/3d-model/model_great_hammerhead_shark/scene.gltf" />

ここを親のコンポーネントからurlを渡せるようにして、いろんなサメを表示できるようにしていきます。



# 参考

https://zenn.dev/redpanda/articles/01b5ce98a632fd

https://qiita.com/btt_17/items/6d2a59826b6a10685486

https://zenn.dev/web_life_ch/articles/b8e641b1ea3a20