LoginSignup
19
3

More than 1 year has passed since last update.

Three.js, gsapを使って発表シミュレーターを作ってみたんですよ

Last updated at Posted at 2021-12-11

スクリーンショット 2021-12-09 10.00.23.png

概要

記念すべき初投稿に、発表シミュレーターを作ってみました。

何かを発表したい時、ありませんか?
クイズの正答、好きな食べ物、日頃言えていないこと....色々ありますね。
そんな時に役立つアプリを作ってみましたので、ご紹介いたします。

伝えたいこと

Three.js, gsap未経験の私でもそれっぽいものを作れたので、
その経験や知識を共有できたらと思ってます。
アプリやソースコードも公開しているので、
Three.js, gsapまわりを使ってみたい方にとって参考になれば幸いです。

発表シミュレーターについて

以下のリンクで公開しています。

ソースコードはこちらです。

使い方

  1. 発表したいテキストを入力欄に入力する
  2. 「テキスト決定」ボタンを押す
  3. 心の準備ができたら、「シミュレートする」ボタンを押す(音が出ます。ご注意ください)

たったこれだけ。好きな言葉で発表してみましょう。

環境について

  • JavaScript
  • react(create-react-appを使用)
  • Three.js
  • gsap
  • npm 6.14.4
  • vercel

ライブラリ自体については、npmでインストールしました。

  npm install three
  npm install gsap

コード解説

アプリの一部を抜粋しながら、Three.js, gsapについて解説していきます。
※以降を書き進めてもアプリは完成しないので、アプリをいじりたい場合はソースコードのクローンをオススメします。

Three.jsでは、3Dでのレンダリングが可能です。
画面のレンダリングで用意するものは以下です。

  • レンダラー
  • シーン(空間)
  • 物体
  • カメラ

レンダラー ・ シーン

まず、基礎部分として以下のコードでレンダラーシーンを生成します。

src/js/App.js
import { WebGLRenderer, Color, Scene } from 'three';

const App = () => {
  const animate = (text) => {
    /* 基礎部分の生成 */
    const { innerWidth, innerHeight } = window;
    // レンダラーを生成
    const renderer = new WebGLRenderer();
    // レンダラーのサイズを設定
    renderer.setSize(innerWidth, innerHeight);
    const canvas = document.getElementById('canvas-container');
    canvas.appendChild(renderer.domElement);
    // シーンを生成
    const scene = new Scene();
    scene.background = new Color('black');
  }
}

① 以下でレンダラーを生成。サイズはウインドウの幅に設定しています。

src/js/App.js
    /* 基礎部分の生成 */
    const { innerWidth, innerHeight } = window;
    // レンダラーを生成
    const renderer = new WebGLRenderer();
    // レンダラーのサイズを設定
    renderer.setSize(innerWidth, innerHeight);
    const canvas = document.getElementById('canvas-container');
    canvas.appendChild(renderer.domElement);

シーンを生成。背景は黒で設定しています。

src/js/App.js
    // シーンを生成
    const scene = new Scene();
    scene.background = new Color('black');

これで何もない空間が出来上がります。

テキスト

次は、空間に物体を追加します。
このアプリでは、物体として

  • テキスト
  • ライト
  • (ライトを当てる対象)

を追加しますが、
本記事ではテキストについて解説します。
以下コードです。

src/js/App.js
import fontJson from '../assets/fonts/GenEi_Antique_Pv5_Regular.typeface.json';
import { TextModel } from './text';

const App = () => {
  const animate = (text) => {
    /* 省略 */

    // 文字列を生成
    const textModel = new TextModel({ 
      text, // ユーザーが入力したテキスト
      fontJson // json形式のfont
    });
    // シーンに追加
    scene.add(textModel.mesh);
  }
}
src/js/text.js
import { MeshPhongMaterial, Mesh } from "three";
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';

export class TextModel {
  constructor({ text, fontJson, positionY = 15 }) {
    const loader = new FontLoader();
    // フォントをロードする
    const font = loader.parse(fontJson);

    this.positionY = positionY;
    this.geometry = this.makeGeometry(text, font);
    this.materials = this.makeMaterials();
    this.mesh = this.makeMesh(this.geometry, this.materials);
    this.align(this.positionY);
  }

  // 配置を直す
  align = (positionY = 0) => {
    if (!this.geometry || !this.mesh) return;
    this.geometry.center();
    this.mesh.position.setY(positionY);
  }

  // 文字列メッシュを作成して返す
  makeMesh = (geometry, materials) => {
    // ジオメトリとマテリアルを渡してメッシュを生成。これをシーンに追加することで画面に表示できる。
    return new Mesh(geometry, materials);
  }

  // 文字自体に関するオブジェクトを作成して返す
  makeGeometry = (text, font) => {
    // font: json形式のfont
    // size: サイズ
    // height: 高さ
    // curveSegments: 曲線上のポイントの数。数値が高いと文字オブジェクトが丸くなる
    // bevelxxxxx: bevelは傾斜とか斜角という意味だが、ここら辺は見ながら設定
    return new TextGeometry(text, {
      font: font,
      size: 5,
      height: 3,
      curveSegments: 1,
      bevelEnabled: true,
      bevelThickness: 1,
      bevelSize: 1,
      bevelOffset: 0,
      bevelSegments: 1
    });
  }

  // 材質のオブジェクトを作成して返す
  makeMaterials = (textColor = Math.random() * 0xffffff, backgroundColor = 0x000000) => {
    // [ 文字の材質, 文字まわり(背景)の材質 ]
    // MeshPhongMaterial: 光沢のある材質, MeshLambertMaterial: マットな材質, MeshStandardMaterial: 中間くらいの材質
    // color: 色
    return [
      new MeshPhongMaterial({ color: textColor }),
      new MeshPhongMaterial({ color: backgroundColor })
    ];
  }
}

③ geometry, materialsを元にmeshを生成。

  • geometry: 文字自体のオブジェクト。
  • materials: 文字の材質についてのオブジェクト。
  • mesh: geometry, materialsを元に作られた文字のオブジェクト。
src/js/text.js
    this.geometry = this.makeGeometry(text, font);
    this.materials = this.makeMaterials();
    this.mesh = this.makeMesh(this.geometry, this.materials);

④ 以下コードでシーンに追加。

src/js/App.js
    // シーンに追加
    scene.add(textModel.mesh);

以上でテキストの生成は終わりです。

カメラ

次はカメラを追加します。
以下コードです。

src/js/App.js
import { CameraDriver } from './camera';

const App = () => {
  const animate = (text) => {
    // カメラを生成
    const cameraDriver = new CameraDriver({ 
      cameraParams: { fov: 45, aspect: 2, near: 0.1, far: 100 }, 
      textModel, // テキストのインスタンス
      lightDriver, // ライトのインスタンス
      targetDriver, // ライトを当てる対象のインスタンス
      playAudio // 音楽を流す関数
    });
  }
}
src/js/camera.js
import { PerspectiveCamera } from "three";
import { gsap } from "gsap";

/* カメラを動かす */
export class CameraDriver {
  constructor({ cameraParams, textModel, lightDriver, targetDriver, playAudio }) {
    this.cameraModel = new CameraModel(cameraParams);
    this.textModel = textModel;
    this.lightDriver = lightDriver;
    this.targetDriver = targetDriver;
    this.animationComplete = false;
    this.playAudio = playAudio;
  }

  animate = () => {
    /* 省略 */
  }
}

/* カメラ */
export class CameraModel {
  constructor({ fov, aspect, near, far, position }) {
    this.camera = this.makeCamera({ fov, aspect, near, far, position });
  }

  // カメラを生成
  makeCamera = ({ fov = 45, aspect = 2, near = 0.1, far = 100, position = { x: 0, y: 0, z: 0 } }) => {
    // fov: 視野角
    // aspect: アスペクト比
    // near: 設定した近さまでカメラに映る
    // for: 設定した遠さまでカメラに映る
    const camera = new PerspectiveCamera(fov, aspect, near, far);
    // カメラのポジションをセット
    camera.position.set(position.x, position.y, position.z);
    return camera;
  }
}

⑤ ここでカメラを生成。
なお、カメラについてはシーンに追加する必要はありませんが、アニメーション部分で必要になります。

src/js/camera.js
  // カメラを生成
  makeCamera = ({ fov = 45, aspect = 2, near = 0.1, far = 100, position = { x: 0, y: 0, z: 0 } }) => {
    // fov: 視野角
    // aspect: アスペクト比
    // near: 設定した近さまでカメラに映る
    // for: 設定した遠さまでカメラに映る
    const camera = new PerspectiveCamera(fov, aspect, near, far);
    // カメラのポジションをセット
    camera.position.set(position.x, position.y, position.z);
    return camera;
  }

※PerspectiveCameraの他にもカメラの種類があります。詳しくは以下リンクを参照ください。
Three.jsのカメラ

アニメーション

最後にアニメーションです。

src/js/App.js
import { CameraDriver } from './camera';

const App = () => {
  const animate = (text) => {
    /* 省略 */

    // 毎フレームごとに動く
    const render = () => {
      /* 省略 */

      renderer.render(scene, cameraDriver.cameraModel.camera);

    // 次のアニメーションをリクエスト
      requestAnimationFrame(render);
    }
    // 次のアニメーションをリクエスト
    requestAnimationFrame(render);
  }
}

⑥ ①で生成したレンダラーによってレンダリング。第一引数はシーン、第二引数にはカメラをとります。

src/js/App.js
      renderer.render(scene, cameraDriver.cameraModel.camera);

⑦ requestAnimationFrameによってrender関数が約毎フレームごとに動きます。

src/js/App.js
    // 次のアニメーションをリクエスト
    requestAnimationFrame(render);

render関数の中にアニメーション処理を書くことになるのですが、
gsapを使えば毎フレームごとの管理をせずに済む&複雑な動きが可能になるので、便利です。
gsapを使用したコードは以下です。

src/js/camera.js
import { PerspectiveCamera } from "three";
import { gsap } from "gsap";

/* カメラを動かす */
export class CameraDriver {
  /* 省略 */

  // カメラのアニメーションを実行
  animate = () => {
    if (!this.cameraModel || !this.textModel || !this.lightDriver || !this.targetDriver) return;
    const { camera } = this.cameraModel; // カメラ
    const { positionY } = this.textModel; // テキストのy座標
    const { complete } = this.lightDriver; // カメラのアニメーション終了をライトに通知するための関数
    const { front, back } = this.targetDriver.targetModel; // ライトを当てる対象
    // timelineはgsapの機能。アニメーションのタイムラインを生成。
    // gsap.timelineには以下のようなオプションも付けられる。
    // repeat: タイムラインを繰り返す回数。-1を設定すると無期限に繰り返す。
    // yoyo: trueの場合ヨーヨーのようにアニメーションを前後する。(1=>2=>3=>2=>1とタイムライン上のアニメーションを往復する)
    // delay: アニメーションを始めるまでの遅延時間
    const timelineCamera = gsap.timeline();
    // .set、.to、(.from, .fromTo)をつなげることでアニメーションを連続して再生できる。
    // .setと.toの違い: 物体を指定の位置まで移動するのは共通だが、.setはアニメーションなし、.toはアニメーションなしで移動する。
    timelineCamera
      .set(camera.position, {
        x: 0, // x座標
        y: 50, // y座標
        z: 0, // z座標
        delay: 0, // 遅延時間
        onComplete: () => { // 動作が完了したら実行する
          this.playAudio(); // 音楽を流す
          camera.lookAt(0, 0, 0); // 指定の位置にカメラを向かせる
        }
      })
      .to(camera.position, {
        x: 0,
        y: 40,
        z: 0,
        duration: 1.8, // アニメーションにかける時間
        delay: 0,
        ease: "sine.inOut", // アニメーションのイージング
      })
      .set(camera.position, {
        x: 15,
        y: positionY,
        z: 40,
        delay: 0,
        onComplete: () => {
          camera.lookAt(10, 10, 0);
        }
      })
      .to(camera.position, {
        x: 15,
        y: positionY,
        z: 10,
        duration: 2,
        delay: 0,
        ease: "sine.inOut",
      })
      .set(camera.position, {
        x: -15,
        y: positionY,
        z: 40,
        delay: 0,
        onComplete: () => {
          camera.lookAt(-10, 10, 0)
        }
      })
      .to(camera.position, {
        x: -15,
        y: positionY,
        z: -10,
        duration: 2.6,
        delay: 0,
        ease: "sine.in",
      })
      .set(camera.position, {
        x: -30,
        y: positionY,
        z: 40,
        delay: 0,
        onComplete: () => {
          camera.lookAt(-30, 10, 0)
        }
      })
      .to(camera.position, {
        x: 30,
        y: positionY,
        z: 40,
        duration: 4.2,
        delay: 0,
        ease: "sine.in",
      })
      .set(camera.position, {
        x: 0,
        y: positionY,
        z: 0,
        delay: 0,
        onComplete: () => {
          camera.lookAt(0, positionY, 0)
          this.animationComplete = true;
        }
      })
      .to(camera.position, {
        x: 0,
        y: positionY,
        z: 40,
        duration: 0.5,
        delay: 0,
        ease: "sine.in"
      }, 'focus') // 第3引数に文字列を入れると、同じ文言の入っているアニメーションと同時に実行する。
      .to(back.target.position, {
        x: 0,
        y: positionY,
        z: 0,
        duration: 0.5,
        delay: 0,
        ease: "sine.in"
      }, 'focus')
      .to(front.target.position, {
        x: 0,
        y: positionY - 2,
        z: 0,
        duration: 0.5,
        delay: 0,
        ease: "sine.in",
        onComplete: () => {
          complete();
        }
      }, 'focus');
    this.timeline = timelineCamera;
  }
}

⑧ 上記コードでカメラのアニメーションを実行しています。アニメーションをざっくり解説すると、

1. カメラを上から下に移動
2. カメラを右後方から右前方へ移動
3. カメラを左後方から左前方へ移動
4. カメラを左後方から右後方へ移動
5. カメラを文字正面から後方へ移動&ライトを当てる対象を中心へ移動

となっており、複雑なアニメーションを一回呼び出すだけで実行してくれます。

以下の部分を数珠つなぎにすることでアニメーションを順番に実行しています。

src/js/camera.js
      .to(camera.position, {
        x: 0, // x座標
        y: 40, // y座標
        z: 0, // z座標
        duration: 1.8, // アニメーションにかける時間
        delay: 0, // 遅延時間
        ease: "sine.inOut", // アニメーションのイージング
      })

振り返り

Three.js、gsapの触りの部分を寄せ集めるだけで、
それっぽいアニメーションを作ることができてとても良かったです。
リッチな表現技法として、これらの技術を学んで損はなかったと思います!
何より見た目で成果がわかるので、モチベーションもずっと保ててました。
まだまだ知識が浅すぎるので、もっと深く学んでいきたいです。
(あと、発表シミュレーターのテキスト入力のUIもなんとかしたい)

以上です。
ビバ、アニメーション!!!

参考資料

Three.jsとGSAPを組み合わせてボックスを回転させるだけ
最新版で学ぶThree.js入門 手軽にWebGLを扱える3Dライブラリ
Three.jsのカメラ
GSAP(基本機能)
フロントエンドから始めるアニメーション 最強のライブラリGSAP3を手に入れよう
GSAP to, from, fromTo, setの使い方

19
3
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
19
3