SpineでモーショントレースしたかったからAfter Effectsから無理矢理json書き出してやってみた

表題の通り。Spineはいわゆる手付けでモーションをつけていくわけだけれど、さっとしたアニメーションちょっとめんどう。

ところでAfter Effectsにはモーションスケッチって機能があります。時間を進めつつオブジェクトをくりくりっと動かすと、その機動を記録してアニメーション出来るようにしてくれます。

これってめちゃくちゃ便利じゃないっすか。例えばダンスモーションの手、足、腰、頭なんかをこいつで追いかければ擬似的なモーショントレースになるんじゃないのかしら? って思ったらもうやってみるしかない。

Spineのjsonの中身を見てみるわよ!!

SpineはファイルをUnityとかで扱うためにjsonとして出力する機能があります。その中身を覗いてみるとかなりシンプルな構造だとわかりました。基本的には前のキーフレームから相対的な移動量で今の位置を決める、の連続です。

これは、いけそうです!!

ただちょっと癖もあって、ボーン構造もまた相対位置で記録してるんですよ。これの何が面倒くさいかって、例えば「前のボーンからX50ずれてますよー」ってあっても、前のボーンが90度ずれてたら、実際の見た目にはY50ずれてるように見えちゃう。これが実際のアニメーションにも反映される。

め・・・・めんどくせええええええ!!!

・・・まあここはある程度妥協していくことにしました・・・。

Spineのボーン構造をAfter Effectsに読み込む

上で中身を見たボーン構造をAfter Effectsに読み込みます。このときいちいち全てのボーンを読み込んでも死ぬだけなので、代表的な物だけを持ってきます。つまり手足と腰と頭って感じ。

画像1

一昔前のゲームみたい。白い点が手足、ピンクの点が腰と頭で。右上のピンクが頭に相当するんですけれど、そうするとずれてますね? これが上記で言ったボーンの角度によってずれた結果となってます。まあ今回はテストだし、本格運用するときに必要だったら修正しようと思います。

  for (var i = 0; i < json.bones.length; i++) {
   var nBone = json.bones[i];
   if ((nBone.name.match(/controller/) && !nBone.name.match(/reverse/)) || (nBone.parent == "root" && nBone.name.match(/target/))) {
     makeShapeSquare(nBone.name, 50, nBone.color, nBone.x + calcX, (nBone.y * -1) + calcY);
   }
 }

スクリプトはこんな感じ。jsonを読み込み名前でコントローラーを担当するボーンかどうかを判定、四角を作成しています。after effectsのx,y値と、Spineのx,y値はちょっと考え方が違うのでその修正もここでしています。

簡単なアニメーションを書き出してみる!

さて早速やってみましょう。

画像2

何も考えていません。わちゃわちゃ動かしてるだけ。

で、これをSpine用のjsonにする。

ループで各レイヤーとキーフレームを回りつつ値を取得、前のXからの相対位置、最初にずらしたXを元に戻して収容します。頭は90度ずれてるのがわかってるのでもう決め打ちでxとyを反転させています。さあどうなるか。

画像3

モーショントレース用に作ったアニメーション項目がきちんとできていますね!

画像4

お、おお? おおお? 動いてはいるけれどすごい暴れている!! 怖い!!!

と、とりあえずはまあ各ボーンきちんとうごいているようです・・・ね?うん。実際に何かモーショントレースしてみましょう。れっつトライ☆

実際に・・・やってみるぜ!

題材はラブアンドジョイにしました。特に理由はありません。テンションをあげたかっただけです。

After Effectsにあげて、動画を再生しながらその上でモーションスケッチします。ここは結構地道かな・・・?うん。

・・・・ん? あれ? モーションスケッチって背景動かしながら使えないの? まじかー!!!

まあ、After EffectsにはモーショントラックやMochaなど、強力なトレース機能がいくつかあります。それらを駆使しつつ手作業でやっていってやるぜ。

うん、まあいいか。この使い方でも。

画像5

地道にトレスした結果。ううん・・・・あなたにラブアンドジョイが見えるだろうか。

画像6

キーはこんな感じに。大量なのは私の要領がちょっと悪かったから。後半は結構改善した・・・がやはり多い。このフローは改善の余地が大いにありそうだというのが所感。リアルさを出すにはいいのかな? とも。

出力してみる。

画像7

うわあああああ!?

ううん、移動量がおかしい感じ。プログラムを調整します。完全におかしい動きである(見たらわかる)。

修正。前のキーとの比較で絶対値と比較しないといけないのに相対値と比較してて数字が馬鹿になってた。感じでした。わーお。

・・・・と思ったらボーンの移動量、絶対値くさい。うーん? どういうことだろう。相対値ではあるんだけれど、「前のキーフレームからの相対位置」ではなくって、設定モードのボーン位置からの相対値をずっと見ている・・・感じらしい!! ということがわかりました。ほほう。

画像8

おお?おお?ラブアンドジョイしてるのでは? してるのでは?????

動きがおかしいのは関節設定の関係ですな。ikですね。あとは見切り発車だったので初期位置の位置合わせとかのズレかなーって感じ。ともあれ当初の目的はまあ達成かなって感じですかね。やったぁ。

一応使用には耐えないと思いますがスクリプト乗っけて起きます。使い方とかはコード読み解く感じでオナシャス!!

こうした方が良い感じになるよってのとかあったら大歓迎!!

本体

//@include "./my_functions.jsx"
var comp = app.project.activeItem;
var calcX = comp.width / 2;
var calcY = comp.height - (comp.height / 4);

var fObj = File.openDialog('ファイルを選択');
if (fObj != null) {
 var res = fObj.open('r'); //読み込み専用で開く
 if (res) {
   var txt = fObj.read();
 }
}
json = JSON.parse(txt);

var cnf = confirm("create bones = yes \n animate export = no");
if (cnf == true) {
 for (var i = 0; i < json.bones.length; i++) {
   var nBone = json.bones[i];
   if ((nBone.name.match(/controller/) && !nBone.name.match(/reverse/)) || (nBone.parent == "root" && nBone.name.match(/target/))) {
     makeShapeSquare(nBone.name, 50, nBone.color, nBone.x + calcX, (nBone.y * -1) + calcY);
   }
 }
}
else {
 for (var i = 0; i < comp.numLayers; i++) {
   var nlayer = comp.layer(i + 1);
   d(nlayer.name);
   var prevX = undefined;
   var prevY = undefined;
   for (var k = 0; k < nlayer.position.numKeys; k++) {
     npotionkval = nlayer.position.keyValue(k + 1);
     npotionkval[1] = npotionkval[1];
     npotionktime = nlayer.position.keyTime(k + 1);
     if (json.animations.motionTrace == undefined) json.animations.motionTrace = {};
     if (json.animations.motionTrace.bones == undefined) json.animations.motionTrace.bones = {};
     if (json.animations.motionTrace.bones[nlayer.name] == undefined) json.animations.motionTrace.bones[nlayer.name] = {};
     if (json.animations.motionTrace.bones[nlayer.name].translate == undefined) json.animations.motionTrace.bones[nlayer.name].translate = [];
     if (json.animations.motionTrace.bones[nlayer.name]["translate"][k] == undefined) json.animations.motionTrace.bones[nlayer.name].translate[k] = {};
     var nowkey = json.animations.motionTrace.bones[nlayer.name]["translate"][k];

     nowkey["x"] = (npotionkval[0] - calcX) / 2;
     nowkey["y"] = (npotionkval[1] - calcY) / 2;
     nowkey["time"] = npotionktime;
     // prevX = npotionkval[0];
     // prevY = npotionkval[1];
     if (nlayer.name.match(/head_controller/)) {
       var tempY = nowkey["y"];
       nowkey["y"] = nowkey["x"];
       nowkey["x"] = tempY;
     }
     nowkey["y"] = nowkey["y"] * -1;
   }
 }
 jsontext = JSON.stringify(json);
 var myFile = new File();
 myFile = myFile.saveDlg("Choose your duration export location", "Text File:*.json");
 d("fileopen")
 myFile.open("w");
 d("mode")
 myFile.write(jsontext);
 d("write")
 myFile.close();
}

my_functions

function d(obj) {
   $.writeln(obj);
}
function makeShapeSquare(name, square, color, x, y) {
   if (name == undefined) name = "square";
   if (square == undefined) square = 10;
   if (color == undefined) color = "ffffff"
   if (x == undefined) x = 0;
   if (y == undefined) y = 0;
   var comp = app.project.activeItem;
   var myShapeLayer = comp.layers.addShape();
   myShapeLayer.name = name;
   myShapeLayer.position.setValue([x, y, 0])
   var shapeProperty = myShapeLayer.property('ADBE Root Vectors Group');
   var myShapePath = shapeProperty.addProperty('ADBE Vector Shape - Group');

   //var myShapeGraphic = shapeProperty

   var myShape = new Shape()
   myShape.vertices = [[0, 0], [square, 0], [square, square], [0, square]];
   myShape.closed = true; //パスを閉じるかどうか
   //myShape.addProperty('ADBE Vector Graphic - Fill');
   myShapePath(2).setValue(myShape);
   var myShapeFill = shapeProperty.addProperty('ADBE Vector Graphic - Fill');
   myShapeLayer.content("Fill 1").color.setValue(parseRgb(color))
}
function parseRgb(code) {
   var code = code.replace(/[^0-9]/, "");
   var red = parseInt(code.substring(0, 2), 16);
   var green = parseInt(code.substring(2, 4), 16);
   var blue = parseInt(code.substring(4, 6), 16);
   return [red, green, blue]
}



この記事が気に入ったらサポートをしてみませんか?