「MOT1995∞2016」用に作成したAfterEffectsスクリプト紹介

この記事は<a href="http://qiita.com/advent-calendar/2016/aftereffects">AfterEffects Advent Calendar 2016</a>の8日目の記事です。
昨日の記事は @yama_ko さんによる<a href="http://qiita.com/yama_ko/items/e216e993ead894003268">「細かすぎて伝わらないプリレンダリング最適化選手権」</a>です。
<h1>MOT1995∞2016</h1>

東京都現代美術館が2016年5月30日から休館になるにあたり、東京都現代美術館リニューアル休館イベントとして、映像プロジェクション「MOT1995∞2016」が上映されました。内容は、東京都現代美術館に今までの展示のポスターや、メッセージを映し出すというシンプルなものだったのですが、その制作の一部に携わり、作業を効率化するためにAfterEffectsのスクリプトをいくつか作ったので、その紹介をしたいと思います。

著作権の関係で、実際の素材を用いてスクリプトの紹介ができないのですが、ご容赦ください。

「MOT1995∞2016」の実際の映像は下記から見ることができます。今からメイキングをする箇所は動画内1分30秒くらいの、たくさんのキャラクターが上から降ってくるとこです。
 https://www.youtube.com/watch?v=mktvcD8nctI

映像を見ていただいてわかる通り、あまりプロジェクションマッピングっぽい演出はなく、のんびりまったりと美術館に映像を映し出す形となっております。

<h1>演出を考える</h1>
制作期間が非常に短かったため、AfterEffectsでできる範囲でどんな演出ができるかを考えました。
キャラクターはすべてレイヤーにわかれていて、AfterEffectsに読み込むとこんな感じになります。
<img width="500" alt="cap_01" src="https://qiita-image-store.s3.amazonaws.com/0/148607/b99e3a1e-8baf-8d20-2f3b-f35426f77229.jpeg">
図形ひとつひとつがキャラクターの代わりで、すでにレイアウトがされている状態です。

実際の作業では、Illustratorのレイヤーがたくさん並んでました。
今回はこちらで素材を用意したので、AfterEffectsのシェイプレイヤーを使って再現してます。

アイデアは色々でたのですが、キャラクターがかわいい感じだったので、画面外上からタイミングばらばらに降ってくるのはどうだろうと考えました。
チームにアイデアを提案してオッケーが出たので、どうやって効率よく制作を行うかを考えます。

読み込んだレイヤー数は、多いもので1面200レイヤー近いものもありました。
「MOT1995∞2016」では、それが3面あったので、全部合わせると400近いレイヤーになると思います。
これら全てに手付けでキーフレームアニメーションを打っていくのは精神的にきついです。
タイミングの調整をあとから行うのも数が多いので大変だし、もともとの座標が違うので同じアニメーションをつけようにもキーフレームのコピペができません。
なので、AfterEffectsのスクリプトを書いてなんとかしようと考えました。

<h1>AfterEffectsのスクリプト（jsx）を書く</h1>
キーフレームアニメーションをスクリプトで適用させる方法として、僕の中でですが、2パターンあります。
ひとつは、プログラムによってアニメーションを制御する方法です。

例1 ）0秒,1秒,2秒で回転プロパティにキーフレームを打つ

![sample.gif](https://qiita-image-store.s3.amazonaws.com/0/148607/59e749c5-e809-14c5-5701-1292bca3ceca.gif)

var myRotationProperty = app.project.item(1).layer(1).property("Rotation");
myRotationProperty.setValueAtTime(0, 0);
myRotationProperty.setValueAtTime(1, 90);
myRotationProperty.setValueAtTime(2, 0);

こんな感じでプログラム内でキーフレームを打つパラメータや時間を指定してアニメーションをつけることができます。
もちろんイージーイーズの指定もできるのですが、これでバウンドのアニメーションをつけるのは大変です。
そこで今回は、手付けでバウンドアニメーションをつけたものをScriptに書き出して、それを適用させることにしました。


例2 ）手付けでアニメーションしたものをScriptへ持っていく

とりあえずバウンドさせるアニメーションを作ります。
しかし、さっきも言ったように今回アニメーションを適用するレイヤーは全て位置情報がバラバラなので、ひとつ作ったアニメーションをコピペすることができません。
そこで、スライダー制御を用いて、元の位置からの差分でバウンドさせるアニメーションをつけることにしました。
スライダー制御をレイヤーに適用して、適用したレイヤーの位置に以下のようなExpressionを打ちます。

[transform.position[0],transform.position[1]] + [0,effect("スライダー制御")("スライダー")];
これでスライダー制御の値を動かすことで、Y座標が動きます。スライダー制御にキーフレームを打ってバウンドさせます。

![animation_test_comp.gif](https://qiita-image-store.s3.amazonaws.com/0/148607/ceecc0fb-3233-d3fb-7aac-1701c7028d16.gif)

できました。
より正確にキーフレーム情報をScriptへ変換するため、1frameごとにキーフレーム変換をします。
スライダー制御に空のExpressionを打ちます。あとは、プロパティを選択してアニメーション＞キーフレーム補助＞エクスプレッションをキーフレームに変換をすることで、1frameごとにキーフレームを打つことができます。

<img width="500" alt="cap_05" src="https://qiita-image-store.s3.amazonaws.com/0/148607/7c725fed-d180-4940-b27c-3caa3f8214ec.jpeg">

![capture_06.JPG](https://qiita-image-store.s3.amazonaws.com/0/148607/b2162be4-2e3e-977e-8331-c48419b62fde.jpeg)

変換するとこんな感じです。アニメーションが止まったあとのキーフレームはいらないので削除しちゃいましょう。

次にこのスライダー制御エフェクトと、位置に打ったExpressionをScriptへ持っていきます。
持っていく手段として、bry-fulさんのスクリプト作成補佐スクリプトを使用しました。

参考：AfterEffectsユーザーのための、プログラミング入門 その7　スクリプト作成補佐スクリプト
http://ae-users.com/jp/tutorials/2011/08/after-effects%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E3%80%81%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%85%A5%E9%96%80%E3%80%80%E3%81%9D-7/comment-page-1/

記事内にあるように、選択しているプロパティのキーフレームを書き出したり、シェイプコンテンツをScriptへ持っていったりすることができます。
では、さっそくScriptへ変換してみます。縦に長くなってしまったので画像化してます。

トランスフォームをScriptへ変換
<img width="500" alt="cap_01" src="https://qiita-image-store.s3.amazonaws.com/0/148607/393a723d-9d35-b6fd-5767-d6adfb1a39e7.jpeg">

エフェクトをScriptへ変換
<img width="500" alt="cap_01" src="https://qiita-image-store.s3.amazonaws.com/0/148607/72225676-c302-a72c-23be-2a04662ce027.jpeg">

これで、キーフレームのアニメーションやExpressionをScriptへ書き出すことができました。
Expressionを適用するだけなら、Scriptでの適用の仕方を調べてExpressionの文章だけ変えてあげればいいですが、手付けにアニメーションをしたものをScriptに変換する方法はこれ以外自分は知らないので、本当にありがたいです。

このようにして取得したScriptを組み込んで、欲しい結果を得るためのScriptを組んでいきます。
全てのレイヤーにスライダー制御を適用させること、位置にExpressionを適用させることの他に、タイミングを重複しないようにバラバラにする処理を追加、また、全体の不透明度を一括で管理する必要があったため、Nullレイヤーを作成し、そのNullの不透明度とキャラクターの不透明度が一致するようにExpressionも追加しています。

できあがったスクリプトは以下になります。

<h1>FallAnimHelper.jsx</h1>

さきほどのコンポジションに対して制作したスクリプトを走らせます。
タイミングがバラバラになってるのが見てわかると思います。

<img width="500" alt="cap_01" src="https://qiita-image-store.s3.amazonaws.com/0/148607/02828abe-0027-58ae-701a-c12779d52206.jpeg">


アニメーション結果はこんな感じ。手付けだと大変ですが、スクリプトだと一発でここまでこれます。

![sampleMotion.gif](https://qiita-image-store.s3.amazonaws.com/0/148607/8a51b4af-219f-e6b8-857b-6b4a268f97ce.gif)


```js:FallAnimHelper.jsx

//ctrl+Z使用
app.beginUndoGroup("begin");

//compを選択
var comp = app.project.activeItem;
if(comp == null){
    alert("コンポジションを選択してください。");
    }

var num = comp.numLayers;
var random = [];

//重複なしに乱数を発生させるプログラム
function random_num(num){
	//乱数の配列生成
	for (i = 1; i < num; i++) {
	  random[i]=Math.floor(Math.random()*num);
	}
	var j;
	random[0]=Math.floor(Math.random()*num);

　  //配列内に重複してるものがあるか探査．あったら重複しなくなるまで乱数発生
	for (i = 1; i < num; i++) {
	  j = 0;
	  while(j<i){
	    while(random[i] == random[j]){
	      random[i]=Math.floor(Math.random()*num);
	      j=0;
	    }
	    j++;
	  }
	}
	return random;
}

random_num(num);

//コンポ内のレイヤー全てにエフェクト・エクスプレッションを適用する
for(var i = 1; i < num+1; i++){
    for(var i = 1; i < num+1; i++){
    setFx(comp.layer(i));
    setTransform(comp.layer(i));
}}

//スタートタイムをずらす
//全体のスタートからエンドまで12秒、アニメーション分の時間を確保するためには
//全体のレイヤー数 * スライドさせる時間 + ひとつのレイヤーのアニメーションにかかる時間 = 全体のアニメーションにかかる時間

var allAnimationTime = 10;
var oneAnimationTime = 1;
var slideTime;

for(var i =1; i < comp.numLayers; i++){
    slideTime = (allAnimationTime - oneAnimationTime) / comp.numLayers;
    //alert(random[i]+1);
    comp.layer(random[i]+1).startTime = slideTime * i;    
}

//新規ヌルを1番上に作成。その下の素材レイヤーの不透明度にExpression
var nullLayer = comp.layers.addNull();
nullLayer.name = "不透明度調整用ヌル";
var myProperty = nullLayer.opacity;
myProperty.setValue(100);
var myOpacity = myProperty.value;
for(var i = 1; i < num+1; i++){
    for(var i = 1; i < num+1; i++){
    setOpacity(comp.layer(i+1));//+1してるのは1番上にヌルを追加したから
}}

setOpacityKey(nullLayer);

//平面レイヤーを作成
var solidLayer = comp.layers.addSolid([1,1,1], "BG", comp.width, comp.height, 1.0);
solidLayer.moveToEnd();
setFx2(solidLayer);
solidLayer.moveToEnd();




//スライダー制御適用
function setFx(lyr)
{
	var eg = lyr.property("ADBE Effect Parade");
	//---------
	var fx = eg.addProperty("ADBE Slider Control");
	fx.name = "スライダー制御"
	fx.enabled = true;
	fx.property("ADBE Slider Control-0001").setValueAtTime(0,-730);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.03333333333333,-725.92381485579);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.06666666666667,-714.823560380829);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.1,-697.966231737825);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.13333333333333,-676.234219143676);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.16666666666667,-650.263521264427);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.2,-620.521964147505);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.23333333333333,-587.355766089839);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.26666666666667,-551.017870701964);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.3,-511.684735855297);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.33333333333333,-469.464815109743);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.36666666666667,-424.399811441832);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.4,-376.457855023172);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.43333333333333,-325.515018191993);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.46666666666667,-271.315993639274);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.5,-213.390362669824);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.53333333333333,-150.85523043828);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.56666666666667,-81.8445531241621);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.6,-1);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.63333333333333,-43.0541204745853);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.66666666666667,-73.5531978276097);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.7,-96.2659585464614);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.73333333333333,-113.178165123004);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.76666666666667,-125.476327723082);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.8,-133.924082411926);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.83333333333333,-139.034064065305);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.86666666666667,-141.155532085221);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.9,-140.521900323846);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.93333333333333,-137.276606977853);
	fx.property("ADBE Slider Control-0001").setValueAtTime(0.96666666666667,-131.485431887896);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1,-123.138342211122);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.03333333333333,-112.140535083832);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.06666666666667,-98.2883962478512);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.1,-81.2186228235074);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.13333333333333,-60.2997094862093);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.16666666666667,-34.3740629821409);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.2,-0.9999901534444);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.23333333333333,-31.3196310405863);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.26666666666667,-52.8828982853851);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.3,-68.1669965560001);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.33333333333333,-78.4202617980778);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.36666666666667,-84.3153036660452);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.4,-86.191921711278);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.43333333333333,-84.1553101165575);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.46666666666667,-78.1050180583299);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.5,-67.7135560182582);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.53333333333333,-52.3423181229906);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.56666666666667,-30.8361952731593);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.6,-0.99998523016654);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.63333333333333,-26.9144407718063);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.66666666666667,-41.7620910770906);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.7,-45.8756432037258);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.73333333333333,-39.8608683819302);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.76666666666667,-24.5614291907411);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.8,-1.00001600065343);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.83333333333333,-11.550694182107);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.86666666666667,-14.8195291726616);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.9,-11.1827794339528);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.93333333333333,-1.00001600065343);
	fx.property("ADBE Slider Control-0001").setValueAtTime(1.96666666666667,-1.00001600065343);

}

//位置にExpression
function setTransform(lyr)
{
	lyr.threeDLayer = false;

	var tf = lyr.property("ADBE Transform Group");
	//var anc = tf.property("ADBE Anchor Point");
	//anc.setValue([0,0,0]);

	var pos = tf.property("ADBE Position");
	pos.expression = 
	"[transform.position[0],transform.position[1]] + [0,effect(\"スライダー制御\")(\"スライダー\")];\r\n";

	pos.expressionEnabled = true;

}

//不透明度エクスプレッション
function setOpacity(lyr)
{
	lyr.threeDLayer = false;

	var tf = lyr.property("ADBE Transform Group");
	var opa = tf.property("ADBE Opacity");
	opa.setValue(100);
	opa.expression = 
	"thisComp.layer(\"不透明度調整用ヌル\").transform.opacity\r\n";

	opa.expressionEnabled = true;

}

//塗り
function setFx2(lyr)
{
	var eg = lyr.property("ADBE Effect Parade");
	//---------
	var fx = eg.addProperty("ADBE Fill");
	fx.name = "塗り"
	fx.enabled = true;
	fx.property("ADBE Fill-0001").setValue(0);//塗りつぶしマスク

	fx.property("ADBE Fill-0007").setValue(0);//すべてのマスク

	fx.property("ADBE Fill-0002").setValueAtTime(3,[0/255,0/255,0/255,255/255]);
	fx.property("ADBE Fill-0002").setValueAtTime(4.66666666666667,[255/255,255/255,255/255,255/255]);

	fx.property("ADBE Fill-0006").setValue(0);//反転

	fx.property("ADBE Fill-0003").setValue(0);//水平ぼかし

	fx.property("ADBE Fill-0004").setValue(0);//垂直ぼかし

	fx.property("ADBE Fill-0005").setValue(1);//不透明度

}

//不透明度にキーフレームを
function setOpacityKey(lyr)
{
    lyr.threeDLayer = false;
	var tf = lyr.property("ADBE Transform Group");
    var opa = tf.property("ADBE Opacity");
	opa.setValueAtTime(13.6666666666667,100);
	opa.setValueAtTime(15,0);
}

//ctrl+Z使用
app.endUndoGroup();
```

<h1>ExchangeStartTime.jsx</h1>
おまけです。FallAnimHelper.jsxを制作したことで、コンポジションにあるレイヤーの数がいくつであっても、重複しないタイミングで上から落ちてくるアニメーションを簡単に制作することができるようになりました。
でも、ここで思ったのはタイミングを入れ替えるのが面倒だということです。
落ちてくるタイミングはランダムなので、序盤に画面右側ばっかり落ちてくる、とかいったことがありえます。
そこで、選択している2つのレイヤーのスタートタイムを入れ替えるスクリプトを作りました。

```js:FallAnimHelper.jsx
//UI設定
    var myPanel = new Window('window', 'exchangeStartTime_v0.1', [100,100,400,200],{resizeable:true});
    Stc1 = myPanel.add("statictext",[30,5,270,45]);
    Stc1.text = "スタートタイムを入れ替えるレイヤーを\n2つ選択して実行してください。";
    Stc1.visible = true;
    
    myPanel.Btn = myPanel.add ("button" , [130,55,180, 85],"click");
    
    myPanel.center();
    myPanel.show();

myPanel.Btn.onClick = function (){

app.beginUndoGroup("begin");

//compを選択
var comp = app.project.activeItem;
if(comp == null){
    alert("コンポジションを選択してください。");
    }

var tempLength = comp.selectedLayers.length;
var selectedLayers = [];

if(tempLength == 2){
for(var i = 0; i < tempLength; i++){
selectedLayers[i] = comp.selectedLayers[i];
}}
else alert("スタートタイムを入れ替えるレイヤーを2つ選択してください。多いか少ないかしてます。");

if(tempLength == 2){
    var startTimeA = selectedLayers[0].startTime;
    var startTimeB = selectedLayers[1].startTime;
    selectedLayers[0].startTime = startTimeB;
    selectedLayers[1].startTime = startTimeA;
}

//ctrl+Z使用
app.endUndoGroup();
}
```

頻度高めで使うことを考えてボタン式にしました。
こういうツールは必要に応じてたくさん作っておくといいと思います。

<h1>まとめ</h1>
スクリプトがちょっと書けるようになると、自分専用のft-toolbarもどきを作ってみたり、今回のような面倒な作業をスクリプト化してみたり、と色んな効率をあげることができます。
プログラミングがよくわからんって人も、ダウンロードできるjsxファイルを読んでみたり、アニメの道具箱などのサイトを読んで始めてみてください。

明日は @matsurai25 さんの「最近のフロントエンド技術でAdobe ExtendScriptを書く」です！