Oops!!

プログラミングとかのラフなブログ

Next.js + react-three-fiberを使って.glbファイルを表示した時のメモ

Next.js + react-three-fiberを使って.glbファイルを表示した時のメモ

Next.jsでやる必要は全くなかったんですが、手ぐせでcreate-next-appをしてしまったので、Next.js上でThree.jsを学んでいます。

もし開発に制約がないのであればcreate-react-appで始める事をお勧めします。

あと、いきなりreact-three-fiberを使っての開発だとわからないことやドキュメントを読み進めにくい部分もあると思ったので、Parcel + three.jsの環境も用意して検証しながら進めてます。

モジュールのimportができない

three.jsのmoduleをimportしようとするとCannot use import statement outside a moduleというエラーが出ます。 今回は、three/examples/jsm/controls/OrbitControlsをimportする部分などで発生していたので、next/dynamicで解決しました。

/* _app.tsx */
import { AppProps } from 'next/app'
import { Canvas } from 'react-three-fiber'
import dynamic from 'next/dynamic'

const Controls = dynamic(() => {
  return import('components/Controls')
}, { ssr: false })

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Canvas>
      <Controls />
      <gridHelper />
      <Component {...pageProps} />
    </Canvas>
  )
}

Suspenseは便利だけどまだ安定リリースはされてない

<Suspense>は読み込みが終わるまでfallbackを表示し、読み込みが終わったら中のComponentを表示してくれます。 今回はdynamic importを多様するので使ってみることにしましたが、ドキュメントを読むと2021/2/4時点でまだ実験的な機能だということです。

サスペンスを使ったデータ取得(実験的機能)

import { Suspense } from 'react'
import dynamic from 'next/dynamic'

const SampleModel = dynamic(() => {
  return import('components/SampleModel')
}, { ssr: false })

export default function Page() {
  return (
    <Suspense fallback={null}>
      <SampleModel />
    </Suspense>
  )
}

glbファイルを読み込んで表示する

glbファイルはpublic/gltf/Xbot.glbに入れています。読み込むためにnext.config.jsにfile-loaderの設定をします。

/* next.config.js */
module.exports = {
  webpack: (config, options) => {
    config.module.rules.push({
      test: /\.(glb|gltf)$/,
      use: {
        loader: 'file-loader',
      }
    })
    return config
  },
}

表示だけであれば<primitive>のobjectにsceneを渡せば表示されます。

走らせる場合の設定に関してはまだ完全に理解できてるわけではありません。

今回のモデルの場合は、gltf.animationsの中のnameが「run」というのがたまたま4番目にきていたのでgltf.animations[3]を決め打ちで入力しています。

しかし他の動きに変更すると挙動がおかしくなるanimationもあるので、モデル側の設定とThree.js側の設定が噛み合っていないのだと思います。あやしいのはclockですがまだどうしたらいいのかわかっていません。

/* components/SampleModel.tsx */
import { useState, useEffect, useRef } from 'react'
import * as THREE from 'three'
import { useLoader, useFrame } from 'react-three-fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

const position = [0, 0, 0]
const scale = [1, 1, 1]

type Props = {}
const SampleModel: React.FC<Props> = (props) => {
  const gltf = useLoader(GLTFLoader, "/gltf/Xbot.glb")
  const clock = useRef(new THREE.Clock());
  const [mixer] = useState(() => new THREE.AnimationMixer(gltf.scene))

  useEffect(() =>  {
    if (!mixer) return
    const action = mixer.clipAction(gltf.animations[3])
    action.play()
  }, [mixer, gltf])

  useFrame(() => {
    mixer.update(clock.current.getDelta())
  })
  
  return (
    <primitive 
      object={gltf.scene} 
      dispose={null}
      position={position}
      scale={scale}
    />
  )
}

export default SampleModel

まとめ

今までのWebは平面上(二次元)上にコンポーネントを配置していくので、三次元のイメージがし難く、二次元のイメージに引っ張られ結構苦戦しています。

まだ学習は続ける予定なのでreact-three-fiberで詰まったこととかあったら、ここに追記していきます。

プロフィール画像

すずき ゆうた

愛知県でフリーランスのフロントエンド・エンジニアをしています。Reactを用いた開発が得意です。 他にもプロジェクトマネジメントや組織マネジメントも行ってきました。エビデンスのない事でも自分の経験から書いていくので話半分くらいでお願いします。