コンポジションのタイムリマップを再生開始デュレーションを指定して再生させる「ゲーム制作テクニックで遊ぶAfter Effects」(キャラクターモーションの項)をスクリプト化。
[概要]
複数のアニメーションパターンを1コンポジションにまとめて管理できます。決まったデュレーション単位で複数のアニメーションパターンを用意し、キーフレームで任意のタイミングで何番目のアニメーションパターンを再生するか指定します。
キャラクターのモーションなど、パターンを切り替える際に便利です。
[使い方]
- スクリプトを実行
- 開いたUIに1アニメーションが何秒か、何パターン作る予定か、新規コンポジション名を入力
- タイムラインをアクティブ状態で[OK]ボタンをクリック
- 新規追加されたコンポジション内にアニメーションを用意する
- 「#モーション番号」エフェクトで何番目のアニメーションを再生するかをキーフレームで制御
[注意点]
タイムリマップ制御のため、親コンポジション、アニメーションをセットするコンポジションともにfpsは小数点がないようにしてください。ズレます。
また、1つのモーションを再生し切るので、極端に短いモーションであっても規定の秒数の残りにはデフォルトのアニメーションを埋め込むなどして空きフレームはないよう配置しておいてください。
[解説]
//////////////////////////////////////////////////
// 指定デュレーションを1セットとして
// キーフレームで再生するパターンを指定するスクリプト。
// n秒ごとの動きのパターンを作り
// そのコンポジションの「#モーション番号」で指定番目の動きを再生。
// 例)
// 1セット5秒なら…
// 「#モーション番号」を2にセットすると
// 5:00〜9:29までを再生。
//
// ※注意点として動きをセットするコンポジションのfpsは
// 30など割り切れる数に。小数点あるとズレてしまう。
//////////////////////////////////////////////////
var scName="tRemapPlayer@Selector";
var itms=app.project.items;
var actCmp=app.project.activeItem;
var seconds=1;
var targetLyr;
var actCmpAlert="コンポジションを追加したいタイムラインをアクティブにしてクリック";
var wobj=new f_showDialog;
w.show();
/*
===============
--- GUI準備 ---
===============
*/
function f_showDialog(){
w=new Window('palette{properties:{resizeable:false,}}',scName);
w.center();
var gr_ui=w.add('group',undefined,"uiGroup");
gr_ui.orientation='column';
gr_ui.alignChildren='left';
var gr_dur=gr_ui.add('group',undefined,"durGroup");
gr_dur.orientation='row';
gr_dur.alignChildren='left';
st_dur=gr_dur.add('statictext',[0,0,130,30],"1セットの秒数:",);
ed_dur=gr_dur.add('edittext',[0,0,50,30],"1",{multiline:false});
var gr_patterns=gr_ui.add('group',undefined,"patternsGroup");
gr_patterns.orientation='row';
gr_patterns.alignChildren='left';
st_patterns=gr_patterns.add('statictext',[0,0,130,30],"アニメーションパターン数:",);
ed_patterns=gr_patterns.add('edittext',[0,0,50,30],"3",{multiline:false});
var gr_compname=gr_ui.add('group',undefined,"compnameGroup");
gr_compname.orientation='row';
gr_compname.alignChildren='left';
st_compname=gr_compname.add('statictext',[0,0,130,30],"新規コンポジション名:",);
ed_compname=gr_compname.add('edittext',[0,0,200,30],"アニメーションパターン",{multiline:false});
var gr_apply=w.add('group',undefined,"applyGroup");
gr_apply.orientation='row';
gr_apply.alignChildren='left';
b_go=gr_apply.add('button',[0,0,200,30],"OK");
b_go.helpTip=actCmpAlert;
}
/*
===============
--- ボタン操作 ---
===============
*/
b_go.onClick=function(){
app.beginUndoGroup(scName);
actCmp=app.project.activeItem;
if(actCmp==null){
alert(actCmpAlert);
}else{
f_folderSet();
f_tRemapPlayerAtSelector(seconds,targetLyr);
w.close();
}
app.endUndoGroup();
}
function f_folderSet(){
if(actCmp==null)return;
var addFolderName="tRemapPlayer@Selector";
var targetFolder="";
var folderFlag=false;
for(var i=1;i<=itms.length;i++){
if(itms[i].name==addFolderName){
folderFlag=true;
targetFolder=itms[i];
}
}
if (folderFlag==false){
targetFolder=itms.addFolder(addFolderName);
}
seconds=ed_dur.text;
if(seconds==null)return;
var patterns=ed_patterns.text;
if(patterns==null)return;
var targetComp=itms.addComp(ed_compname.text,actCmp.width,actCmp.height,actCmp.pixelAspect,seconds*patterns,actCmp.frameRate);
targetComp.parentFolder=targetFolder;
var compMarker=[];
var color=[3,4];// ラベルカラー 3->ピンク,4->アクア
for(i=0;i<patterns;i++){
compMarker[i]=new MarkerValue(i+1);
compMarker[i].duration=seconds;
compMarker[i].label=color[i%color.length];
targetComp.markerProperty.setValueAtTime(seconds*i,compMarker[i]);
}
targetLyr=actCmp.layers.add(targetComp);
}
function f_tRemapPlayerAtSelector(seconds,targetLyr){
var fxName1="#モーション番号";
var fxName2="モーションを維持";
var addFx1,addFx2;
var frm=actCmp.frameDuration;
var fx=targetLyr.property("ADBE Effect Parade");
addFx1=fx.addProperty("ADBE Slider Control");
addFx1.enabled=false;
addFx1.name=fxName1;
addFx1(1).setValue(1);
addFx1(1).addKey(0);
addFx1(1).setInterpolationTypeAtKey(1,KeyframeInterpolationType.HOLD,KeyframeInterpolationType.HOLD);
addFx1(1).expression='Math.floor(value)';
var addFx2=fx.addProperty("ADBE Checkbox Control");
addFx2.enabled=false;
addFx2.name=fxName2;
targetLyr.timeRemapEnabled=true;
targetLyr.outPoint=actCmp.duration;
targetLyr.timeRemap.removeKey(2);
targetLyr.timeRemap.setValueAtTime(targetLyr.inPoint,0);
targetLyr.timeRemap.setValueAtTime(targetLyr.inPoint+(Math.round(seconds/frm)-1)*frm,(Math.round(seconds/frm)-1)*frm);
targetLyr.timeRemap.expression=
'var dur='+seconds+';//リマップ長さ秒;\r'+
'var nearKey=effect("'+fxName1+'")(1).nearestKey(time);\r'+
'var motionIndex=(time<nearKey.time)?nearKey.index-1:nearKey.index;\r'+
'if(motionIndex>0){\r'+
' var motionKey=effect("'+fxName1+'")(1).key(motionIndex);\r'+
' var nowframe=timeToFrames(time+inPoint)-timeToFrames(motionKey.time);\r'+
' var d=motionKey*dur-dur;\r'+
' if((framesToTime(nowframe)/dur>=1)&&(effect("'+fxName2+'")(1)==0)){\r'+
' d=0;\r'+
' }\r'+
' valueAtTime(framesToTime(nowframe)%dur)+d;\r'+
'}else{\r'+
' 0;\r'+
'}';
alert('コンポジションの中は整数fpsか確認\r29.97など小数点あるとズレ発生');
}
GUIを準備
function f_showDialog(){
w=new Window('palette{properties:{resizeable:false,}}',scName);
w.center();
var gr_ui=w.add('group',undefined,"uiGroup");
gr_ui.orientation='column';
gr_ui.alignChildren='left';
var gr_dur=gr_ui.add('group',undefined,"durGroup");
gr_dur.orientation='row';
gr_dur.alignChildren='left';
st_dur=gr_dur.add('statictext',[0,0,130,30],"1セットの秒数:",);
ed_dur=gr_dur.add('edittext',[0,0,50,30],"1",{multiline:false});
var gr_patterns=gr_ui.add('group',undefined,"patternsGroup");
gr_patterns.orientation='row';
gr_patterns.alignChildren='left';
st_patterns=gr_patterns.add('statictext',[0,0,130,30],"アニメーションパターン数:",);
ed_patterns=gr_patterns.add('edittext',[0,0,50,30],"3",{multiline:false});
var gr_compname=gr_ui.add('group',undefined,"compnameGroup");
gr_compname.orientation='row';
gr_compname.alignChildren='left';
st_compname=gr_compname.add('statictext',[0,0,130,30],"新規コンポジション名:",);
ed_compname=gr_compname.add('edittext',[0,0,200,30],"アニメーションパターン",{multiline:false});
var gr_apply=w.add('group',undefined,"applyGroup");
gr_apply.orientation='row';
gr_apply.alignChildren='left';
b_go=gr_apply.add('button',[0,0,200,30],"OK");
b_go.helpTip=actCmpAlert;
}
/*
===============
--- ボタン操作 ---
===============
*/
b_go.onClick=function(){
app.beginUndoGroup(scName);
actCmp=app.project.activeItem;
if(actCmp==null){
alert(actCmpAlert);
}else{
f_folderSet();
f_tRemapPlayerAtSelector(seconds,targetLyr);
w.close();
}
app.endUndoGroup();
}
GUIの準備については正攻法です。
79行目(上記40)、OKボタンをクリックした際の処理としては、下のif文でタイムラインを選択中でなければアラート表示して終わります。
パーツを入れるフォルダを準備する
function f_folderSet(){
if(actCmp==null)return;
var addFolderName="tRemapPlayer@Selector";
var targetFolder="";
var folderFlag=false;
for(var i=1;i<=itms.length;i++){
if(itms[i].name==addFolderName){
folderFlag=true;
targetFolder=itms[i];
}
}
if (folderFlag==false){
targetFolder=itms.addFolder(addFolderName);
}
seconds=ed_dur.text;
if(seconds==null)return;
var patterns=ed_patterns.text;
if(patterns==null)return;
var targetComp=itms.addComp(ed_compname.text,actCmp.width,actCmp.height,actCmp.pixelAspect,seconds*patterns,actCmp.frameRate);
targetComp.parentFolder=targetFolder;
var compMarker=[];
var color=[3,4];// ラベルカラー 3->ピンク,4->アクア
for(i=0;i<patterns;i++){
compMarker[i]=new MarkerValue(i+1);
compMarker[i].duration=seconds;
compMarker[i].label=color[i%color.length];
targetComp.markerProperty.setValueAtTime(seconds*i,compMarker[i]);
}
targetLyr=actCmp.layers.add(targetComp);
}
アニメーションパターンを登録するコンポジションを新規作成するので、プロジェクトパネルが散らからないよう専用のフォルダを準備し、その中に新規コンポジションを移動します。
コンポジションを準備する
89行目(上記7)、for文でプロジェクトパネル内の全アイテム名を調べ、「tRemapPlayer@Selector」というフォルダ名が既にあればその中に、もしくは同名のフォルダがなければ.addFolder()で新規フォルダを作成し、スクリプトを実行する度に同名フォルダが増えないようにしています。
104行目(上記22)は.addComp()でGUIから受け取った設定を流し込みます。
.addComp("コンポジション名",幅,高さ,ピクセルアスペクト比,デュレーション,fps);
新規コンポジションのデュレーションは「1アニメーションの秒数」*「何パターン作るか」です。
.parentFolderでどのフォルダに格納するか指定できます。
107行目(上記25)から、コンポジションに気配りをします。
アニメーションパターンを準備しやすいよう、コンポジションマーカーで区切りをわかりやすくガイド表記します。
※After Effects CC2017からスクリプトでコンポジションマーカーにアクセスできるようになりましたので取り入れました。
マーカーはセットするプロパティを準備→.markerProperty.setValueAtTime()でぶち込むという工程を踏みます。
new MarkerValue()で箱を用意、.durationでマーカーにデュレーションを設定できるので、1フレームだけでなく伸ばせます。
また、視認性を確保するため、マーカーの色を変えています。.label=数値で色を指定します。
設定により変わりますが、マーカー設定の上から番号で指定。なし=1、アクア=4といった具合です。
targetComp.markerProperty.setValueAtTime(seconds*i,compMarker[i]);
にてパターン秒ごとにパターン数分、ぶちこみます。
ラベルカラーはとりあえず2色で見やすいかなとcolor=[3,4];でピンクとアクアだけにしてあります。欲しい場合はこの配列内の数値を増やしてください。
次のfor文内のcompMarker[i].label=color[i%color.length];でi%color.lengthの部分でfor文が回ってもcolor[0]と[1]を繰り返すようにしてあります。
エクスプレッション記事「エクスプレッションtimeで点滅とその他の演算子を知る」で学んだ剰余、演算子の「%」で割り算の余りが使えますね。
この関数の最後、
targetLyr=actCmp.layers.add(targetComp);
にて指定コンポジションにこのコンポジションを挿入します。
メイン処理
function f_tRemapPlayerAtSelector(seconds,targetLyr){
var fxName1="#モーション番号";
var fxName2="モーションを維持";
var addFx1,addFx2;
var frm=actCmp.frameDuration;
var fx=targetLyr.property("ADBE Effect Parade");
addFx1=fx.addProperty("ADBE Slider Control");
addFx1.enabled=false;
addFx1.name=fxName1;
addFx1(1).setValue(1);
addFx1(1).addKey(0);
addFx1(1).setInterpolationTypeAtKey(1,KeyframeInterpolationType.HOLD,KeyframeInterpolationType.HOLD);
addFx1(1).expression='Math.floor(value)';
var addFx2=fx.addProperty("ADBE Checkbox Control");
addFx2.enabled=false;
addFx2.name=fxName2;
targetLyr.timeRemapEnabled=true;
targetLyr.outPoint=actCmp.duration;
targetLyr.timeRemap.removeKey(2);
targetLyr.timeRemap.setValueAtTime(targetLyr.inPoint,0);
targetLyr.timeRemap.setValueAtTime(targetLyr.inPoint+(Math.round(seconds/frm)-1)*frm,(Math.round(seconds/frm)-1)*frm);
targetLyr.timeRemap.expression=
'var dur='+seconds+';//リマップ長さ秒;\r'+
'var nearKey=effect("'+fxName1+'")(1).nearestKey(time);\r'+
'var motionIndex=(time<nearKey.time)?nearKey.index-1:nearKey.index;\r'+
'if(motionIndex>0){\r'+
' var motionKey=effect("'+fxName1+'")(1).key(motionIndex);\r'+
' var nowframe=timeToFrames(time+inPoint)-timeToFrames(motionKey.time);\r'+
' var d=motionKey*dur-dur;\r'+
' if((framesToTime(nowframe)/dur>=1)&&(effect("'+fxName2+'")(1)==0)){\r'+
' d=0;\r'+
' }\r'+
' valueAtTime(framesToTime(nowframe)%dur)+d;\r'+
'}else{\r'+
' 0;\r'+
'}';
alert('コンポジションの中は整数fpsか確認\r29.97など小数点あるとズレ発生');
}
125行目(上記7)から、アニメーションパターン選択用のキーフレームをセットするスライダー制御を準備します。「#モーション番号」です。
「#モーション番号」スライダー制御
.setValue(1)とaddKey(0)でインポイントに値1のキーフレームをセット、.setInterpolationTypeAtKey(1,KeyframeInterpolationType.HOLD,KeyframeInterpolationType.HOLD);でキーフレームを停止に変えます。
.setInterpolationTypeAtKey(キーインデックス,インのキーフレームタイプ,アウトのキーフレームタイプ);
何個目のキーフレームを処理するかキーインデックスで指定、引数の2,3個目はキーフレームのタイプはリニア、ベジェ、停止で指定しますが、
KeyframeInterpolationType.LINEAR
KeyframeInterpolationType.BEZIER
KeyframeInterpolationType.HOLD
インとアウトの2箇所指定する必要があります。
132行目(上記14)、「#モーション番号」のキーフレームは「何番目のアニメーションを再生開始するか」なので、小数点があるとモーションの途中から再生されてしまうため、Math.floor()で小数点を切り捨てて整数にするエクスプレッションを仕込みます。
「モーションを維持」チェックボックス制御
134行目(上記16)から、もう一つエクスプレッション制御を追加します。
これは「#モーション番号」のアニメーションパターンを再生し終わった場合…
- デフォルトの1番目のパターンに戻るか
- 「#モーション番号」のパターンを維持するか
選べる夢の機能です。
維持した場合、特にキャラクターなどを配置している場合、1つめのアニメーションパターンにデフォルトパターンを準備しておけば、他のモーションを再生したあとにデフォルトパターンに自動で戻せます。
維持しなければ切り替えた「#モーション番号」のループを維持します。
タイムリマップの設定、エクスプレッション
138行目(上記20)、.timeRemapEnabled=true;でタイムリマップをONにし、
139行目(上記21)、大抵、アニメーションパターンのコンポジションはメインのコンポジションより短いと思うのでアウトポイントを伸ばしておきます。.outPoint=actCmp.duration;でコンポジションと同じデュレーションに伸ばします。
最後に本命のエクスプレッションです。
GUIで入力させた秒数や、追加したエフェクト名を反映したエクスプレッションとしてぶちこみます。
大筋は「ゲーム制作テクニックで遊ぶAfter Effects」(キャラクターモーションの項)と同じですが、デフォルトのモーションに戻るという仕掛けをブラッシュアップしています。
var dur=1;//リマップ長さ秒;
var nearKey=effect("#モーション番号")(1).nearestKey(time);
var motionIndex=(time<nearKey.time)?nearKey.index-1:nearKey.index;
if(motionIndex>0){
var motionKey=effect("#モーション番号")(1).key(motionIndex);
var nowframe=timeToFrames(time+inPoint)-timeToFrames(motionKey.time);
var d=motionKey*dur-dur;
if((framesToTime(nowframe)/dur>=1)&&(effect("モーションを維持")(1)==0)){
d=0;
}
valueAtTime(framesToTime(nowframe)%dur)+d;
}else{
0;
}
152行目(上記8)です。タイムリマップを「#モーション番号」分オフセットしているだけですが、1パターンのデュレーションを超えたらオフセットを0にしています。
この記事へのコメントはありません。