2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

What is Three.js?

sharkgood.gif

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

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

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

ハンマーヘッドシャークの素材

⬇︎デプロイ先

⬇︎githubURL

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

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でサーバーを起動するとページが表示されます。

必要なライブラリをインストール

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からダウンロードしました。
フォーマットはglTFを選択しましょう。

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

今回はハンマーヘッドシャークの素材をダウンロードしました。

ディレクトリ構成

  • 📂 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を入れます。
テクスチャに関しては後述します。

表示ページを作成

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

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';

型定義や型の拡張を行う

GeoOcean.tsx
type ModelProps = {
  url: string;
};

extend({ Water })

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

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

海を表示する関数定義

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

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はなぜかエラーになるので、コメントアウトしてます。
有識者がいればここの仕様を教えてください。

モデルを表示する関数を定義

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モデルを動かしています。

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

コンポーネントを表示する部分

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

  • カメラの位置をコントロールできる。マウスでカメラの位置を変更したり、zoom操作も可能です。

今後

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

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

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?