🙆

Three.js を React ( Next.js ) で実装する

2022/03/07に公開

Introduction

Three.js は言わずと知れた JavaScript 製の WebGL グラフィックスライブラリだ
公式ドキュメントは充実しているし、何といつの日からか日本語でも読めるようになっている

私は Three.js を勉強していく上で、これを React ( Next.js ) 上で動かせたら随分楽だろうになあと思っている
理由は、最近の React app ( Next app ) の create コマンドがなかなか行き届いていて、すぐにローカルの Web アプリ環境を立ち上げることができるのと、 VSCode の拡張機能の充実から、 Three.js もそうだが、オブジェクトのプロパティ等の補完をいい感じに出してくれること、 TypeScript ・・などなどがある ( Three.js も @types のパッケージがあるので有り難い )
あとは React ( Next.js ) 使いたい個人的な理由など

目的

公式を含めた Three.js のチュートリアルをスムーズに学習するための書き換え
React の方式で楽に記述できそうなところを積極的に変えていくが、チュートリアルの記述方法から逸脱するほどに過度に React 側に寄せることはしない

React ( Next.js ) を使うなら react-three-fiberdrei を使うと良い、ということのようだが、コレ自体はとても興味があるもののまだ Three.js のスの字もわかっていないのでまだ手を出さないでおこうかと思っている

さっそく実装例

これが基本の形となるだろう
コードは ics.media の Three.js 入門「Three.jsのマテリアルの基本」を主に使わせてもらい、それを React Hooks で書き換えた

この入門記事は連載形式になっていて Three.js の基本から応用までを一通り学べるようになっている

書かれたのは 4 年ほど前だが定期的にメンテナンスをしている部分もあるし、内容自体は今も変わっていないので問題なく利用できる

import { useEffect, useRef } from 'react'

import type { NextPage } from 'next'
import * as THREE from 'three'

const Home: NextPage = () => {
  const mountRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const w = 960
    const h = 540

    const renderer = new THREE.WebGLRenderer()

    const elm = mountRef.current

    elm?.appendChild(renderer.domElement)

    renderer.setPixelRatio(window.devicePixelRatio)
    renderer.setSize(w, h)

    const scene = new THREE.Scene()

    const camera = new THREE.PerspectiveCamera(45, w / h, 1, 10000)
    camera.position.set(0, 0, +1000)

    const geometry = new THREE.SphereGeometry(300, 30, 30)

    const loader = new THREE.TextureLoader()

    const texture = loader.load('/texture/earthmap1k.jpg')

    const material = new THREE.MeshStandardMaterial({
      map: texture,
    })

    const mesh = new THREE.Mesh(geometry, material)

    scene.add(mesh)

    const directionalLight = new THREE.DirectionalLight(0xffffff)
    directionalLight.position.set(1, 1, 1)

    scene.add(directionalLight)

    const tick = () => {
      mesh.rotation.y += 0.01
      renderer.render(scene, camera)

      requestAnimationFrame(tick)
    }

    tick()

    return () => {
      elm?.removeChild(renderer.domElement)
    }
  }, [])

  return (
    <div ref={mountRef} />
  )
}

export default Home

ポイントは useRef()useEffect()

コンポーネントがマウントされた後でないと canvas のサイズが取得できないため useEffect() 内で Three.js 関連の描画処理を行っている
appendChild()canvas をマウントし、最後は return の中で removeChild() して解放する

ref を使って canvas をマウントする場所を特定するのだが TypeScript では .currentnull になる可能性があるので ?. を忘れない
また removeChild() の中で .current をそのまま使うと

と Warning が出てしまう
mountRef.current は値が変わってしまうかも知れないから変数にコピーして使ってね」とあるので頭の方で

elm = mountRef.current

とした

まとめ

今回は素の JavaScript で書かれている Three.js チュートリアルを React ( Next.js ) の中で素直に実行できるように最低限の書き換えを行った
React の特性を生かしたもっと動的な実装方法はいくらでもやりようはあるが追い追い順応していこうと思う

Discussion