概要
記念すべき初投稿に、発表シミュレーターを作ってみました。
何かを発表したい時、ありませんか?
クイズの正答、好きな食べ物、日頃言えていないこと....色々ありますね。
そんな時に役立つアプリを作ってみましたので、ご紹介いたします。
伝えたいこと
Three.js, gsap未経験の私でもそれっぽいものを作れたので、
その経験や知識を共有できたらと思ってます。
アプリやソースコードも公開しているので、
Three.js, gsapまわりを使ってみたい方にとって参考になれば幸いです。
発表シミュレーターについて
以下のリンクで公開しています。
ソースコードはこちらです。
使い方
- 発表したいテキストを入力欄に入力する
- 「テキスト決定」ボタンを押す
- 心の準備ができたら、「シミュレートする」ボタンを押す(音が出ます。ご注意ください)
たったこれだけ。好きな言葉で発表してみましょう。
環境について
- 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でのレンダリングが可能です。
画面のレンダリングで用意するものは以下です。
- レンダラー
- シーン(空間)
- 物体
- カメラ
レンダラー ・ シーン
まず、基礎部分として以下のコードでレンダラー
、シーン
を生成します。
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');
}
}
① 以下でレンダラー
を生成。サイズはウインドウの幅に設定しています。
/* 基礎部分の生成 */
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');
これで何もない空間が出来上がります。
テキスト
次は、空間に物体を追加します。
このアプリでは、物体として
- 床
- テキスト
- ライト
- (ライトを当てる対象)
を追加しますが、
本記事ではテキスト
について解説します。
以下コードです。
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);
}
}
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を元に作られた文字のオブジェクト。
this.geometry = this.makeGeometry(text, font);
this.materials = this.makeMaterials();
this.mesh = this.makeMesh(this.geometry, this.materials);
④ 以下コードでシーン
に追加。
// シーンに追加
scene.add(textModel.mesh);
以上でテキストの生成は終わりです。
カメラ
次はカメラ
を追加します。
以下コードです。
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 // 音楽を流す関数
});
}
}
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;
}
}
⑤ ここでカメラ
を生成。
なお、カメラ
についてはシーンに追加する必要はありませんが、アニメーション部分で必要になります。
// カメラを生成
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のカメラ
アニメーション
最後にアニメーションです。
import { CameraDriver } from './camera';
const App = () => {
const animate = (text) => {
/* 省略 */
// 毎フレームごとに動く
const render = () => {
/* 省略 */
renderer.render(scene, cameraDriver.cameraModel.camera);
// 次のアニメーションをリクエスト
requestAnimationFrame(render);
}
// 次のアニメーションをリクエスト
requestAnimationFrame(render);
}
}
⑥ ①で生成したレンダラー
によってレンダリング。第一引数はシーン
、第二引数にはカメラ
をとります。
renderer.render(scene, cameraDriver.cameraModel.camera);
⑦ requestAnimationFrameによってrender関数が約毎フレームごとに動きます。
// 次のアニメーションをリクエスト
requestAnimationFrame(render);
render関数の中にアニメーション処理を書くことになるのですが、
gsapを使えば毎フレームごとの管理をせずに済む&複雑な動きが可能になるので、便利です。
gsapを使用したコードは以下です。
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. カメラを文字正面から後方へ移動&ライトを当てる対象を中心へ移動
となっており、複雑なアニメーションを一回呼び出すだけで実行してくれます。
以下の部分を数珠つなぎにすることでアニメーションを順番に実行しています。
.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の使い方