この記事は「続・Mohoから出力したSVGを制御したい妄想の話」の一部です。
■実物
【GitHub】SourceOf0-HTML/path_control: SVGを制御したい願望
https://github.com/SourceOf0-HTML/path_control
【GitHub Pages】ベクターデータをいじり倒したい気持ち
https://sourceof0-html.github.io/path_control/
■記事一覧
【SVG制御妄想1】SVG解析しないと始まらない
https://qiita.com/flying_echidna/items/5a628db0d652d1558208
【SVG制御妄想2】Mohoから出力したSVGのマスクがバグる
https://qiita.com/flying_echidna/items/3930caf04626deec7bfb
【SVG制御妄想3】連番データをどげんかせんと
https://qiita.com/flying_echidna/items/ded3f3590c3d67fadb86
【SVG制御妄想4】変形させたいよなぁ?
https://qiita.com/flying_echidna/items/188634f35a05bbde9a51
【SVG制御妄想5】ボーンぐりぐり
https://qiita.com/flying_echidna/items/a34648da8a650fe34824
【SVG制御妄想6】助けてマルチスレッド
https://qiita.com/flying_echidna/items/80b101c1a1eedb534137
【SVG制御妄想7】SVGの限界
https://qiita.com/flying_echidna/items/2f53a461c5e6c05109df
■過去記事
【2019-03-06】「SVGでアニメーションさせたいんじゃ」の詳細報告
https://qiita.com/flying_echidna/items/ff3a061f4e348e62cca0
【2020-02-13】Mohoから出力したSVGを制御したい妄想の話
https://qiita.com/flying_echidna/items/da7ecc721650fa9ab651
#ボーンで制御したいんじゃ
ここまでアクションの合成だの、レイヤーの変形だの、やってきたけども。
それじゃ我慢できんのだ。
既存のアクションをリアルタイムで合成処理するのも、いろいろできるかもしれない。
各レイヤーをリアルタイムで変形処理するのも、いろいろできるかもしれない。
でもやっぱ、ボーン制御できた方が、いろんな姿勢取らせられるやん、と。
ちなみに実際に作業途中のつぶやきがこんな感じ。
ボーンに角度制限を付けたった。
— BUN (@BUN_information) August 26, 2020
もうちょいやりようありそうな気はするけど、なんかもう、これでいいかなって気がしてきている(雑 pic.twitter.com/k7fhQkiOwC
カーソルの位置に向かってボーンを制御してる。
んだけど…まあいろいろ大変だったのよ。うん。
#そもそもSVGの中にボーンの情報がないんだが
そりゃそうだ。
本来Mohoから出力する動画や画像は、あくまで結果の出力であって、編集データの保存じゃない。
結果として表示する予定もないボーンの情報が、わざわざ含まれるわけもない。
じゃあどうするのか。
表示すればいいじゃない。
ボーンの配置情報自体は無理矢理出力できるし、親とか強度の設定あたりは別途JSONとかで入れるとして…
— BUN (@BUN_information) February 15, 2020
実行中のボーン制御とマージさせるなら、どういうマージの仕方をするのかは考え所だなぁ… pic.twitter.com/v6jxSO3WYC
ボーンの位置に三角形を別途配置し、ボーンに追従して動くように設定した。
これでSVGでも出力されるようになった。
あとはグループレイヤーにboneと付けておいて、JavaScript側で識別するようにしておき、
三角形の底辺(一番短い辺)をボーンの支点として、向きと角度を算出するようにした。
【GitHub】path_control/path_svg_loader.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/path_svg_loader.js
支点の算出は、202行目から。
let dist1X = posData[1][0] - posData[0][0];
let dist1Y = posData[1][1] - posData[0][1];
let dist1 = dist1X * dist1X + dist1Y * dist1Y;
let dist2X = posData[2][0] - posData[0][0];
let dist2Y = posData[2][1] - posData[0][1];
let dist2 = dist2X * dist2X + dist2Y * dist2Y;
if(dist1 > dist2) {
ret.push({type:"M", pos:[posData[0][0] + dist2X/2, posData[0][1] + dist2Y/2]});
ret.push({type:"L", pos:[posData[1][0], posData[1][1]]});
} else {
ret.push({type:"M", pos:[posData[0][0] + dist1X/2, posData[0][1] + dist1Y/2]});
ret.push({type:"L", pos:[posData[2][0], posData[2][1]]});
}
なんでこんな二度手間を…ぐむむ…とはいえ、まだまだ情報が足りない。
どのボーンがどのボーンと親子関係なのか。
どのパスに影響を与えるのか。
与える影響の度合い(強度)はどれぐらいなのか。
さすがに、この情報をMohoから出力するSVGに含めることはできない。
仕方なくJSON形式の設定ファイルを用意することにした。
手打ちですが何か?
【GitHub】path_control/bones.json ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/resource/bones.json
17行目から。
"bone3_neck": {
"parent": "bone2_clothes",
"feedback": true,
"strength": 0.05,
"maxAngle": 20,
"minAngle": -20
},
この場合だと、
プロパティ | 説明 | 値 |
---|---|---|
(オブジェクト名) | ボーンの名前 | bone3_neck |
parent | 親ボーンの名前 | bone2_clothes |
feedback | 親ボーンにも影響を与えるか | true |
strength | パスの変形範囲 | 0.05 |
maxAngle | 最大回転角度 | 20 |
minAngle | 最小回転角度 | -20 |
って感じ。
あとはこれとは別で…
132行目から。
"flexi": {
"bone7_right_arm": ["bone1_clothes", "bone2_clothes"],
"clothes": ["bone1_clothes", "bone2_clothes"],
"jacket": ["bone1_clothes", "bone2_clothes"],
"neck": ["bone3_neck"],
"layer_head": ["bone4_head"],
"hat_brim": ["bone4_head"],
"left_arm": ["bone5_left_arm", "bone6_left_arm"],
"right_arm": ["bone7_right_arm", "bone8_right_arm"],
"layer_left_leg": ["bone9_left_leg"],
"layer_right_leg": ["bone10_right_leg"],
"hair": ["bone11_hair"]
}
flexiというオブジェクトに、
レイヤー名 : [ボーンの名前, ... ]
を入れてる。
これで、どのレイヤーがどのボーンに影響を受けるのかを設定してる。
Mohoの中で言うところのフレキシ結合の設定に該当する。
他にも、前々回の記事で話題に挙げたスマートボーンというものが存在するので、それの設定もある。
113行目から。
"bone16_pupils_S": {
"smartAction": "pupils",
"smartBase": 0,
"smartMax": 180
},
プロパティ | 説明 | 値 |
---|---|---|
(オブジェクト名) | ボーンの名前 | bone16_pupils_S |
smartAction | 制御対象となるアクションの名前 | pupils |
smartBase | 基準とする角度 | 0 |
smartMax | 最大角度 | 180 |
この場合だと、
ボーンbone16_pupils_Sが0度のとき、アクションpupilsの最初のフレームを参照し、
180度のとき、アクションpupilsの最後のフレームを参照する。
90度のとき、中央のフレームを参照する。
pupilsは瞳の向きのアクションなので、
実際には、0度で右を見て、90度で正面を見て、180度で左を見る、といった感じになる。
#ともあれフォーワードキネマティクスからせねば
フォーワードキネマティクスとは?
小難しい言葉で表現しているけども、考え方は単純。
例えば…
肩を回せば、肩から先にある腕や手が移動する。
肘を曲げれば、肘から先にある手が移動する。
極当たり前のことなんだけど…
プログラムにとっては当たり前じゃなく、実装しないとそうはならない。
昔p5.jsというJavaScriptのライブラリで簡単なものを実装したことがあるので、これで紹介。
OpenProcessingというサービス上で公開しているので、ブラウザが対応していれば実際に動く。
sketch_190830a - OpenProcessing
https://www.openprocessing.org/sketch/748921
カーソルをx軸方向(横)に移動させれば、画面中央を中心に親ボーンが回転する。
カーソルをy軸方向(縦)に移動させれば、親ボーンにくっついてる子ボーンだけが回転する。
こんな感じの動きを、ボーンにさせるわけだ。
前回の記事でアフィン変換の話をしたけども、あれを応用すればできる。
どこを中心に、何をどれだけ回転させるか、をひたすら算出すればいい。
親ボーンの支点を中心に子ボーン全部を回転させた後、
子ボーンの支点を中心に、その子ボーンの子ボーンを回転させて…を繰り返す。
それだけ。
#インバースキネマティクスさせたいよね
インバースキネマティクスとは?
フォーワードキネマティクスの逆?まあ…計算順は逆かな?
こっちはあれこれややこしい。
例えば…
机の上に置かれたコップを持とうとする。
指の位置がコップの表面に来るとして…
それに合わせて、手のひらの向きや角度はどうあるべきなのか。
その時の腕の角度も、肩の角度も、どうあるべきなのか。
そのままコップを持ち上げようとしたら?
どこの関節をどう回転させればいい?
そういう話。
これも昔p5.jsで作ったことがあるので紹介。
sketch_190830b - OpenProcessing
https://www.openprocessing.org/sketch/748924
カーソルの位置に向かって、ボーンの角度を決定してる。
親ボーンの支点は画面中央で固定してるので、カーソルまで届かなくなったら、ピーンと伸びる。
これの計算については、正直自分も理論らしい理論はちゃんと理解できてない。ワロス。
自分がフォーワードキネマティクスやインバースキネマティクスを知ったのが、
「ActionScript 3.0 アニメーション」という本なので、一応紹介しとく。
Flashなき今、改めて購入する人はいないだろうけどね…
ActionScript 3.0 アニメーション | Keith Peters, 永井 勝則 |本 | 通販 | Amazon
https://www.amazon.co.jp/dp/4862460496
投げっぱなしもなんなので、先ほどのOpenProcessingのコードを張る。
function setup() {
createCanvas(S=500,S);
H=S/2;
segment1 = createSegment(0,0);
segment2 = createSegment(H,H);
}
function draw() {
clear();
t = reach(segment1, mouseX, mouseY);
reach(segment2, t.x, t.y);
p = getPin(segment2);
segment1.x = p.x;
segment1.y = p.y;
view(segment1);
view(segment2);
}
function createSegment(x,y) {
return {
x: x,
y: y,
w: H/2,
h: H/8,
angle: 0
};
}
function view(T) {
push();
translate(T.x,T.y);
rotate(T.angle);
rect(0,-T.h/2,T.w,T.h);
pop();
}
function reach(T,x,y) {
dx = x - T.x;
dy = y - T.y;
T.angle = atan2(dy,dx);
pin = getPin(T);
w = pin.x - T.x;
h = pin.y - T.y;
tx = x - w;
ty = y - h;
return {x:tx, y:ty};
}
function getPin(T) {
x = T.x + cos(T.angle) * T.w;
y = T.y + sin(T.angle) * T.w;
return {x:x, y:y};
}
reach()というメソッドで、指定した方向に向かって子ボーンを回転させつつ、最終的に支点がどこに移動したかを返り値で返す。
そしてその支点に向かって、親ボーンを回転させて、支点の位置まで移動させる。
大体そんな感じ。
正直このあたりの処理はもうちょっと改善させたいなと思ってる。
相対的な回転度数で持ちたいんだよなぁ…
あとSVGから角度を算出すると一周オーバーした結果かどうか判別付かないんだよなぁ…
まあその辺は今は置いときましょう。
#ボーンに追従してパスが…どうやって動くんだ??
個人的に一番処理内容の想像が付かず、どうしたらいいもんかさっぱりだった部分。
ボーンをうまく制御できたとしても、それに追従して線がフニャっと曲がってくれなきゃ意味がない。
ボーンとパスとの距離によって、JSONに入れた強度設定を参考にしながら、どれぐらいボーンと同じような位置に来るかを算出すりゃあいいんだろうけど…?
そうは言ってもよくわからん。
なんとなくの単語で調べていくと、やっとこさそれっぽい用語に遭遇した。
「スキニング」「頂点ブレンド」「バーテックスブレンディング」
そうそうそれそれ、みたいな検索結果が出てくる。
その中でも、分かりそうで分からない、少しわかるサイトさんがこちら。
床井研究室 - 第16回 バーテックスブレンディング
http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20091231
うぬぅ。数学赤点偏差値28の自分にはなかなか意味が分からぬ。
結局どうしたのか。9割アドリブ(自力)である。
バグも確認済み。ひどい。
対象となるボーンとパス自体はJSONの情報から紐づけられるからいいとして…
まずは1ボーンと1点の距離の算出だ。
今回ボーンは向きも関係ないので、単純な直線として扱える。
じゃあ直線と点の距離の算出でいい。
検索すりゃあそれっぽいコードが出てくるし、それを参考にしたれ、と。
残念ながら自分が参考にしてたページは現在削除されてるようなので、自分のコードから。
【GitHub】path_control/BoneObj.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/org/BoneObj.js
319行目から。
getInfluence(x0, y0) {
let strength = this.strength;
if(!strength) return 0;
let x1 = this.defState.x0;
let y1 = this.defState.y0;
let x2 = this.defState.x1;
let y2 = this.defState.y1;
let a = x2 - x1;
let b = y2 - y1;
let r2 = a*a + b*b;
let tt = -(a*(x1-x0)+b*(y1-y0));
let dist = 0;
if( tt < 0 ) {
dist = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0);
} else if( tt > r2 ) {
dist = (x2-x0)*(x2-x0) + (y2-y0)*(y2-y0);
} else {
let f1 = a*(y1-y0)-b*(x1-x0);
dist = (f1*f1)/r2;
}
return dist * strength;
};
強度がないなら、このボーンはパスになーんにも影響を与えない。
強度があるなら、ボーンと点との距離を求める。
で、距離に対して強度の値を掛けてる。
この値を「ボーンがパスに与える影響の比率」として扱う。
近ければ近いほど影響がでかい。遠ければ遠いほど影響が小さい。
強度が高ければ、遠くても影響が出てくる。そんな感じになればOK、と。
比率だけ出しても仕方ねぇ。ちゃんとパスに影響を与えねば。
そもそも影響ってなんだ?
回転とか移動とか…じゃあアフィン変換でいいよね、Spriteクラスつこたろ、と。
発想が雑ぅ!!
104行目から。
let sprite = this.effectSprite;
sprite.x = currentPos[0];
sprite.y = currentPos[1];
sprite.anchorX = this.defState.x0;
sprite.anchorY = this.defState.y0;
sprite.scaleY = dist / this.defState.distance;
sprite.rotation = angle - this.defState.angle;
ボーンの支点のデフォルトの座標を回転・拡縮の中心とするために、アンカーポイントして指定。
そこからの移動量を、現在のボーンの支点の座標とする。
ボーンがデフォルトよりも短くなってたら、ちょっと縮小したりしたいから、
デフォルトの長さと、現在の長さで比率を出して使う。
あとはデフォルトの回転量から、現在どれぐらい回転の変化があったか、で回転させる角度を算出。
まあ影響具合はこんなもんでいいでしょう。
さて、じゃあ実際に影響を与えてやろうではないか。
【GitHub】path_control/PathObj.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/org/PathObj.js#L125
109行目から。
static calcFlexiPoints(pathContainer, flexiIDs, points, index = 0, pointsNum = points.length) {
if(!points || points.length > index + pointsNum || pointsNum < 2) return;
for(let i = index; i < index + pointsNum; i += 2) {
if(flexiIDs.length == 1) {
let id = flexiIDs[0];
if(pathContainer.groups[id].strength == 0) continue;
pathContainer.groups[id].effectSprite.getMatrix().applyToPoint(points, i);
continue;
}
let x = points[i];
let y = points[i+1];
let ratioList = [];
let sum = 0;
flexiIDs.forEach(id=>{
let val = pathContainer.groups[id].getInfluence(x, y);
sum += val;
ratioList.push(val);
});
if(sum == 0) continue;
points[i] = 0;
points[i+1] = 0;
flexiIDs.forEach((id, j)=>{
pathContainer.groups[id].effectSprite.getMatrix().multAndAddPoint(1 - ratioList[j]/sum, x, y, points, i);
});
}
};
先に言います。ここバグってます。えぇ。
一応1つのレイヤーに対して、影響を与えるボーンが2個の場合であれば、それっぽい動きをするんだけども、
どうもそれ以上の数のボーンを紐づけると、位置がずれる。
どうにかしようと思って、どうにもできてない。ワロス。
ともあれ何をしてるか、一応解説。
1つのレイヤーに対して、1つのボーンしか影響を与えない場合は、
愚直にそのまま、そのレイヤー内のパス全部に、そのボーンだけの影響を与える。
さっき用意したSpriteを使うだけ。
pathContainer.groups[id].effectSprite.getMatrix().applyToPoint(points, i);
終わり。
それ以上の数のボーンを扱うときは…
とりあえず現在の点の位置をxとyに保存。
その後、対象のボーン全部が、対象の1点に対して、どれぐらい影響を与えられるか、比率の算出と合計を求めてる。
flexiIDs.forEach(id=>{
let val = pathContainer.groups[id].getInfluence(x, y);
sum += val;
ratioList.push(val);
});
ここ。
影響の比率の合計が0だったら、与える影響もクソもないので、無視。
0でなければ…各ボーンの影響を、影響比率分だけ与えて、合計する。
ただし、比率は最大1にしたいので、合計で各比率を割って1に無理やり調整してる。
points[i] = 0;
points[i+1] = 0;
flexiIDs.forEach((id, j)=>{
pathContainer.groups[id].effectSprite.getMatrix().multAndAddPoint(1 - ratioList[j]/sum, x, y, points, i);
});
ここ。
multAndAddPoint()の中身はこっち。
【GitHub】path_control/Matrix.js ・ SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/18b79532596a558ba95cb23404e21107b536292f/src/org/Matrix.js
92行目から。
multAndAddPoint(t, x, y, point, index) {
point[index] += (x * this.a + y * this.c + this.e) * t;
point[index+1] += (x * this.b + y * this.d + this.f) * t;
};
ここの処理は、毎回アフィン変換で1つの座標に対して行ってる処理を参考に作ったもの。
22行目から。
applyToPoint(point, index = 0) {
let x = point[index];
let y = point[index+1];
point[index] = x * this.a + y * this.c + this.e;
point[index+1] = x * this.b + y * this.d + this.f;
};
applyToPoint()との差は、=か+=か、最後にtを掛けてるか、だけの差。
これでとりあえずそれっぽくなったので、今は放置してる。
#バグの原因ってなんだろな?
多分だけど、元の座標として退避したxとyは、2個目のボーン以降は移動した後の座標を使わないといけないんじゃないかな、とは思ってる。
あるいは影響を与える比率の扱い周り。
2個目までで移動した座標をもとに、改めて比率を出しなおさなきゃいけないんじゃないの?とか。
そうなったら比率の合計を事前に求めて無理やり1にする…なんてことできなくなるわけで。
無理やりするもんじゃないんだろうなぁ…多分。
もう一つ気になるのが、調べていて度々出てくる「クオータニオン」と呼ばれるものの存在。
腐っても赤外線カメラでリアルタイムモーションキャプチャーをする研究を多少してた身なので、
それが「四元数」とも呼ばれる、ワールド座標に依存しないタイプの回転処理に使われてる、程度のことはやんわり知ってる。
使ったことはない。そういうのがあるんだなーってだけ。
ただ、今回実装しているのはあくまで2Dの中であって、3Dじゃない。
それに実装してて思うに、各ボーンの相対的な角度を持たせてあーだこーだやるのと、どういう差が出てくるんだろうな?とも思った。
まだまだ勉強が足りませんのぅ。
次の記事:【SVG制御妄想6】助けてマルチスレッド
https://qiita.com/flying_echidna/items/80b101c1a1eedb534137