読むのめんどくさい方は結論から読んで頂ければと。
ハイライト
three.jsでwebglコンテンツを作りはじめてプロジェクトも中盤に差し掛かった頃...
その時の仕様が
- jsonにイベントの名前とフレームを書いておく
- tickerみたいなものが毎フレーム確認してjsonがあったらイベント飛ばす
- イベントを受け取ったらTweenMaxでオブジェクト動かしたりする
という状態になっていた。
jsonは以下のような感じだった気がする。
{
"0": "start",
"40": "test"
}
元々、タイミングの変更とかを楽に検討したかったのでタイミングはjsonに持つことに
したが、TweenMaxのパラメーターとか変更できないし実用性には欠けていて
凝った演出をつくるのしんどい、UnityのAnimationみたいなツール欲しい...
となっていた。
検討
要件として
- GUIでパラメーターが調整できる
- 調整したパラメーターをコードに移植するのが簡単
の2つが上げられ、それ関連のライブラリを調べてみたところ
などが見つかった。
Demoをしばらくみていたが、アニメーションカーブのバラエティが少ない、
イマイチ使い方がわからない、などの問題があって途方に暮れた。
しょうがないので(時間がなかったので1日くらいでできそうな)簡易的なものを
作ることにした。GUIはとりあえずみんな使ってるしdat.GUIでするとして
アニメーションどうしようか考えていった。
結論
gsapを使う準備は元々できていたのでTimelineMaxから試したらdat.GUI
とあわせてなんとなく最低限のものができたのでご紹介
オレオレツールすぎて誰かの役に立つことがあるのかはわからない。
要点は以下3点
TimelineMaxで任意のタイミングで実行する
const tl = new TimelineLite();
tl.to(this.hoge, 2, {x: 10});
tl.to(this.hoge, 2, {y: 10});
TimelineMax,基本的な使い方はこんな感じかと思うんですが、
最後の引数に秒数を入れると時間指定して実行できるらしい。
const tl = new TimelineLite();
tl.to(this.hoge, 2, {x: 10});
tl.to(this.hoge, 2, {y: 10}, 2); //2秒後に実行
tl.to(this.hoge, 2, {z: 10}, 3); //3秒後に実行
みたいな感じで書けるみたいなのを見つけた。
TimelineMaxでのシーケンス管理
progress()関数で0-1の値を投げればタイムラインを任意で
進めたり戻したりできる。
よって、
this.time = 0;
this.obj = new THREE.Mesh(); //mesh等(仮)
const tl = new TimelineLite();
tl.pause(); //自動で進めたくないのでpause();
tl.to(this, 100, {time: 100, ease: Power0.None}); //100秒間のタイムラインをつくる
tl.to(this.obj.position, 2, {x: 10}, 2); //2秒時点から開始
tl.to(this.obj.position, 2, {y: 10}, 4); //4秒時点から開始
const fps = 1000/30;
let progress = 0;
setInterval(()=>{
progress += fps;
tl.progress(progress/100); //1-100sを0-1にmapして代入する
}, fps);
setIntervalとか使って簡単に書くと(動作確認していない)
こんな感じになるかと思う。
勿論、実際には自動で進める時と、pauseと、シーケンスを手動で動かす機能を
dat.GUIを絡めて実装をした。
dat.GUIでjsonをsave/load
http://workshop.chromeexperiments.com/examples/gui/#5--Saving-Values
これで、save/loadができるのはわかっていたが、いまいち使い方わからなかったので
適当にググってこちらを参考に(というかほぼパクって)実装。
歯車のボタンを押すとjsonを吐いてくれるので、
ブラウザでいろいろいじって、jsonをコピーしてソースコードにペースト
するというワークフロー(これでも少し面倒だが...)
Use Case
以下、実際に使っていたコードを
少し改変したもの色々省いてたり、全体のコードの一部だったりするので
これ単体では動かないがなんとなくの雰囲気だけ伝われば、、
import * as THREE from 'three'
import Ticker from '../Ticker'
import TweenMax from 'gsap'
import State from './State'
import dat from 'three-extras/libs/dat.gui.min.js'
/*
とある簡単なステートパターンのステートクラス
begin()で入って、end()で抜ける。
update()が一定のfps(30fps)で呼ばれる
サンプルなのでthis.scene, this.cameraがこのクラス内の何処かで参照できると仮定する
*/
export default class TestState extends State {
constructor(manager){
super(manager);
//イベント名と変更したいパラメーターを先に定義する
this.param = {
test1: {
time: 2.33,
duration: 24,
rotation: Math.PI
},
test2: {
time: 3,
duration: 2.5
},
test3: {
time: 4.66
},
progress: 0.1,
update: this.tweenUpdate,
autoUpdate: true
}
this.total = 27; //トータルタイム
this.frame = 0; //最初のフレーム(もちろん0)
const json = /*なんらかの方法でjsonを入れる*/
this.gui = new dat.GUI({load: json, preset: "First"});
//関係ないデータがjsonに入ってるのでそれ以外をdat.GUIに読ませる。(冗長になるので各値ごとにfolderをつくる)
Object.keys(this.param).forEach((key)=> {
if(typeof this.param[key] !== 'object') return;
const folder = this.gui.addFolder(key);
this.gui.remember(this.param[key]);
Object.keys(this.param[key]).forEach((key2)=> {
folder.add(this.param[key], key2);
});
});
//その他guiに加えたいボタンなどを定義
//guiで変更した内容を反映するボタン
this.gui.add(this.param, 'update');
//自動で進めるかボタン(falseの時は30fpsでタイムラインが進む)
this.gui.add(this.param, 'autoUpdate');
//シーケンスバー
this.gui.add(this.param, "progress", 0.0, this.total).onChange(()=>{
//0 ~ totalタイム(今回は27s)までのバーをタイムラインシーケンスみたいにする。
this.tl.progress(this.param.progress/this.total);
}).listen();
}
begin(){
//timelineつくる。自動で進んでもらっては困るのでpauseする。
this.tl = new TimelineMax();
this.tl.pause();
this.setTween();
}
//タイムラインの設定
setTween(){
//最初に絶対書くもの(0秒~total秒をprogressの0-1とするため)
//easeはリニアにすること。(そうしないと一定間隔でシーケンスが進まないため)
this.tl.to(this, this.total, {"frame": this.total, ease: Power0.easeNone});
this.param.progress = 0;
//cam zoom out
const test1 = this.param.test1;
this.tl.fromTo(this.camera.rotation, test1.duration, {z: 0}, {z: test1.rotation, ease: Power3.easeIn}, test1.time);
//この仕組みの辛いポイント①としては最初にnewして置かなければならないところ(onStartでsceneにaddする)
this.obj = new Obj(); //THREE.Meshとか
const test2 = this.param.test2;
this.tl.to(this.obj, test2.duration, {opacity: 1,
onStart: ()=>{
//onStartでシーンにaddする
this.scene.add(this.obj);
}
}, test2.time);
//任意のタイミングでイベントだけ欲しい場合はcall()を使う。
const test3 = this.param.test3;
this.tl.call(()=>{
console.log("test3")
}, false, false, test3.time);
}
//ブラウザで編集した数値を更新すると呼ばれる。一度tlをkillしてからまた設定する
tweenUpdate(){
super.tweenUpdate();
this.reset();
this.setTween();
}
//この仕組みの辛いポイント②としてシーケンスもどしても一度addしたものは消えたりしないのでreset関数は用意する
reset(){
this.scene.remove(this.obj);
TweenMax.killAll();
}
//state end
end(){
TweenMax.killAll();
}
update(){
if(this.param.autoUpdate == false) return;
super.update();
//guiのprogressをfps 1/30だけ進める(表示は0-27sにしたい)
this.param.progress += 1/30;
//progress(秒)0-1にmapしたのをtimelinemaxのprogressに入れる。
this.tl.progress(this.param.progress/this.total);
}
}
//ステートクラスのもと
import TweenMax from 'gsap'
export default class State{
constructor(manager){
this.manager = manager;
this.tweenUpdate = this.tweenUpdate.bind(this);
}
begin(){
}
end(){
}
tweenUpdate(){
if(this.tl != null){
TweenMax.killAll();
this.tl.kill();
this.tl = null;
}
this.tl = new TimelineMax();
this.tl.pause();
}
onEvent(e){
}
update(){
}
}
感想
もうちょっと便利になりそうな余地が残りつつも
ド短期のプロジェクトの中で最低限の機能を持ったツールとしては
まぁまぁ使えたのでないかと思う。
実際の動きとか、タイミングの調整などをブラウザみながら
チーム内で検討ができたのはよかった。
バグっぽいことがあったとしても自分でつくってたので
なんとか吸収できるのと、ある程度使いづらい面も自分のせいなので黙認できた。