クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【Three.js + Next.js】Three.js再始動!Next.js環境でThree.jsを動かすの巻

Three.jsがNext.js環境で動くか、試したところ、動きました!Three.jsのドキュメントのGet Startedがベースですが、個人的にやってみたかったこともいくつか追加。任意のタグの中にcanvasタグを配置、画面リサイズに対応してcanvasタグもリサイズ、ライティング設定です。それではいきましょう!

【目次】

※参考:【WebGL】Three.jsを習得するためにやったことまとめ(随時更新) - Qiita
※参考:【React】ReactでWebアプリを作るシリーズまとめ(随時更新) - Qiita

つくったもの

黒い空間の中で箱がクルクル回っているだけ(自分にとっては大きな一歩!)

※参考:Test_001: Next + Three.js

ソースコード

※参考:next-three-js-test/Inner_001.tsx at main · ryo-i/next-three-js-test · GitHub

Three.jsのインストール

以前作成した「Next.jsスターターキット」をクローンしてベースにする。

※参考:GitHub - ryo-i/next-app-started


Three.jsをインストール

npm install three

TypeScript用の@typesもインストール

npm install --save-dev @types/three

※参考:Next.jsにThree.jsを導入して表示させるまでの手順【TypeScript】 - Qiita
※参考:three.js docs

インポート

React機能のimport

import React, { useState, useEffect, useRef }  from 'react';

最終的にuseState, useEffect, useRefも使うことになった。

Three.jsのimport

import * as THREE from 'three/src/Three';

ドキュメントにあるようなimport * as THREE from 'three'という書き方だとUncaught Errorになった。

node_modulesの中のReactフォルダなどを見ると直下にindex.jsがある。しかしthreeフォルダ直下にはindex.jsがない。

Three.jsのドキュメントに「ES modules依存」云々の記載がある。また、パス付きで下層ファイルをインポートしている例もあった。そのためthree/src/Threeとパス付きでインポートしたらうまくいった。

import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

※参考:three.js docs

その他、調べたり試したりしたこと(うまくいかず)

※参考:3D canvasを表示 · Issue #2 · ryo-i/next-three-js-test · GitHub

コンポーネント

// Component
function Inner() {
  const [canvasSize, setCanvasSize] = useState(0);
  const figureElm = useRef(null);

  const changeCanvasSize = (canvasElmWidth) => {
    if (canvasElmWidth < 900) {
      setCanvasSize(canvasElmWidth);
    } else {
      setCanvasSize(900);
    }
  }

  useEffect(() => {
    const canvasElmWidth = figureElm.current.clientWidth;
    // console.log('canvasElmWidth(load)', canvasElmWidth);
    changeCanvasSize(canvasElmWidth);

    window.addEventListener('resize', () => {
      const canvasElmWidth = figureElm.current.clientWidth;
      // console.log('canvasElmWidth(resize)', canvasElmWidth);
      changeCanvasSize(canvasElmWidth);
    });
  }, [0]);


  useEffect(() => {
    // three.js
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera( 35, canvasSize / canvasSize, 0.1, 1000 );

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize( canvasSize, canvasSize );

    if (figureElm.current.firstChild) {
      figureElm.current.removeChild( figureElm.current.firstChild );
    }
    figureElm.current.appendChild( renderer.domElement );

    const geometry = new THREE.BoxGeometry( 1, 1, 1 );
    const material = new THREE.MeshStandardMaterial( { color: 0xff0000 } );
    const cube = new THREE.Mesh( geometry, material );
    scene.add( cube );

    camera.position.z = 5;

    const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
    scene.add( light );

    function animate() {
      requestAnimationFrame( animate );

      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;

      renderer.render( scene, camera );
    };

    animate();
  }, [canvasSize]);

  // JSX
  return (
    <Figure ref={figureElm}></Figure>
  );
}

基本的には下記のThree.jsドキュメント「はじめてみよう」のコードがベースになっている。

※参考:three.js docs


それに加えて下記の機能を追加した

  • 任意のタグの中にcanvasタグを配置
  • 画面リサイズに対応してcanvasタグもリサイズ
  • ライティング設定

任意のタグの中にcanvasタグを配置

Three.jsドキュメントのままだとフッターより下、bodyの閉じタグあたりにcanvas要素が追加された。これを任意のタグの中に配置したい。

current.appendChild()を使う方法が多そうに思ったのでそちらの方法をとった
※参考:three.js docs

(他にWebGLRenderer()のcanvasオプションで指定する方法もあった)
※参考:three.js docs


figureElmuseRef()を設定(初期値はnull

 const figureElm = useRef(null);

figureElmにcanvasタグを追加

    if (figureElm.current.firstChild) {
      figureElm.current.removeChild( figureElm.current.firstChild );
    }
    figureElm.current.appendChild( renderer.domElement );

後述するcanvasリサイズ設定によって、画面をリサイズする再レンダリングのたびにcanvasタグが次々と作られてしまった。

appendChild()時にfirstChildでcanvas要素の存在チェックしてremoveChildで削除する方法だとうまくいった

※参考:Three.jsでCSS3の3D描画ができた! | スターフィールド株式会社

画面リサイズに対応してcanvasタグもリサイズ

canvasタグを固定値ではなく、画面幅を変えるたびにcanvasタグをリサイズしたい。フックを設定して連携した。

フックでcanvasSizeを設定(初期値は0)

const [canvasSize, setCanvasSize] = useState(0);

changeCanvasSize()関数

  const changeCanvasSize = (canvasElmWidth) => {
    if (canvasElmWidth < 900) {
      setCanvasSize(canvasElmWidth);
    } else {
      setCanvasSize(900);
    }
  }

引数をチェックし、900より小さければそのサイズをcanvasSizeに登録(大きければ900を登録)

ページ幅を監視し、変化があったらchangeCanvasSize()を実行

 useEffect(() => {
    const canvasElmWidth = figureElm.current.clientWidth;
    // console.log('canvasElmWidth(load)', canvasElmWidth);
    changeCanvasSize(canvasElmWidth);

    window.addEventListener('resize', () => {
      const canvasElmWidth = figureElm.current.clientWidth;
      // console.log('canvasElmWidth(resize)', canvasElmWidth);
      changeCanvasSize(canvasElmWidth);
    });
  }, [0]);

canvasSizeに変化があったらcanvasSizeの値をsetSize()でcanvasタグをリサイズ

useEffect(() => {
    // three.js
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera( 35, canvasSize / canvasSize, 0.1, 1000 );

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize( canvasSize, canvasSize );

    // 中略
  }, [canvasSize]);

ライティング設定

Three.jsの見本だとこのような影のない見た目になった。

これは「MeshBasicMaterial」というライティング設定が不要だがベタ一色になるマテリアルのため
※参考:three.js docs

その他マテリアル
※参考:【Three.js】いろいろなマテリアルを貼ってみる - クモのようにコツコツと


これを影のある「MeshStandardMaterial」に変更したい
※参考:three.js docs

const material = new THREE.MeshStandardMaterial( { color: 0xff0000 } );

ただ変えるだけだと画面が真っ暗になってしまうため、ライティング設定を追加する
※参考:three.js docs

const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
scene.add( light );

これで影付きの回る箱が現れた!!

※参考:Test_001: Next + Three.js

おわりに

ということで、Next.js環境でThree.jsを動かすことができました。問題なく動くことがわかってよかったです♪

今後やりたいことはいっぱいあって、ここにプールしています。順次、ページを増やして記録していきたく思います。

※参考:今後のやりたいこと · Issue #3 · ryo-i/next-three-js-test · GitHub

  • Tree.jsとしてもどんどん凝ったことをしていきたい
  • Reactフックでinputタグと連携させたい
  • GSAPのアニメにも連携させたい
  • Tone.jsの音と連携さたい
  • Cannon.jsの物理演算と連携させたい
  • ユーザーの操作に反応するインタラクティブなものを作りたい
  • canvas上のユーザー操作で得た情報を外部のタグに連携したい
  • PCとスマホのイベント挙動の違いを理解したい
  • mathオブジェクトのランダムや三角関数と連携
  • パーティクルアニメ
  • 文字を立体化させる
  • APIから読んだ文字を読み込む
  • シェードでマテリアルの形を変えたい(モデリングはゼロから可能?)
  • Blenderのモデリングデータを読み込む

※参考:【WebGL】Three.jsを習得するためにやったことまとめ(随時更新) - Qiita
※参考:【React】ReactでWebアプリを作るシリーズまとめ(随時更新) - Qiita