【SVG制御妄想５】ボーンぐりぐり

この記事は「続・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制御妄想１】SVG解析しないと始まらない
https://qiita.com/flying_echidna/items/5a628db0d652d1558208

【SVG制御妄想２】Mohoから出力したSVGのマスクがバグる
https://qiita.com/flying_echidna/items/3930caf04626deec7bfb

【SVG制御妄想３】連番データをどげんかせんと
https://qiita.com/flying_echidna/items/ded3f3590c3d67fadb86

【SVG制御妄想４】変形させたいよなぁ？
https://qiita.com/flying_echidna/items/188634f35a05bbde9a51

【SVG制御妄想５】ボーンぐりぐり
https://qiita.com/flying_echidna/items/a34648da8a650fe34824

【SVG制御妄想６】助けてマルチスレッド
https://qiita.com/flying_echidna/items/80b101c1a1eedb534137

【SVG制御妄想７】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


#ボーンで制御したいんじゃ

ここまでアクションの合成だの、レイヤーの変形だの、やってきたけども。
それじゃ我慢できんのだ。
既存のアクションをリアルタイムで合成処理するのも、いろいろできるかもしれない。
各レイヤーをリアルタイムで変形処理するのも、いろいろできるかもしれない。
でもやっぱ、ボーン制御できた方が、いろんな姿勢取らせられるやん、と。

ちなみに実際に作業途中のつぶやきがこんな感じ。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ボーンに角度制限を付けたった。<br>もうちょいやりようありそうな気はするけど、なんかもう、これでいいかなって気がしてきている（雑 <a href="https://t.co/k7fhQkiOwC">pic.twitter.com/k7fhQkiOwC</a></p>&mdash; BUN (@BUN_information) <a href="https://twitter.com/BUN_information/status/1298550029615947781?ref_src=twsrc%5Etfw">August 26, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

カーソルの位置に向かってボーンを制御してる。
んだけど…まあいろいろ大変だったのよ。うん。


#そもそもSVGの中にボーンの情報がないんだが

そりゃそうだ。
本来Mohoから出力する動画や画像は、あくまで結果の出力であって、編集データの保存じゃない。
結果として表示する予定もないボーンの情報が、わざわざ含まれるわけもない。

じゃあどうするのか。
表示すればいいじゃない。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ボーンの配置情報自体は無理矢理出力できるし、親とか強度の設定あたりは別途JSONとかで入れるとして…<br>実行中のボーン制御とマージさせるなら、どういうマージの仕方をするのかは考え所だなぁ… <a href="https://t.co/v6jxSO3WYC">pic.twitter.com/v6jxSO3WYC</a></p>&mdash; BUN (@BUN_information) <a href="https://twitter.com/BUN_information/status/1228527554442588160?ref_src=twsrc%5Etfw">February 15, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ボーンの位置に三角形を別途配置し、ボーンに追従して動くように設定した。
これで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行目から。

```Javascript:path_svg_loader.js
    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行目から。

```JSON:bones.json
    "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行目から。

```JSON:bones.json
  "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行目から。

```JSON:bones.json
    "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のコードを張る。

```Javascript
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に入れた強度設定を参考にしながら、どれぐらいボーンと同じような位置に来るかを算出すりゃあいいんだろうけど…？
そうは言ってもよくわからん。

なんとなくの単語で調べていくと、やっとこさそれっぽい用語に遭遇した。
「スキニング」「頂点ブレンド」「バーテックスブレンディング」
そうそうそれそれ、みたいな検索結果が出てくる。

その中でも、分かりそうで分からない、少しわかるサイトさんがこちら。

床井研究室 - 第１６回 バーテックスブレンディング
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行目から。

```Javascript:BoneObj.js
  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行目から。

```Javascript:BoneObj.js
    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行目から。

```Javascript:PathObj.js
  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点に対して、どれぐらい影響を与えられるか、比率の算出と合計を求めてる。

```Javascript
      flexiIDs.forEach(id=>{
        let val = pathContainer.groups[id].getInfluence(x, y);
        sum += val;
        ratioList.push(val);
      });
```

ここ。
影響の比率の合計が0だったら、与える影響もクソもないので、無視。
0でなければ…各ボーンの影響を、影響比率分だけ与えて、合計する。
ただし、比率は最大1にしたいので、合計で各比率を割って1に無理やり調整してる。

```Javascript
      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行目から。

```Javascript:Matrix.js
  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行目から。

```Javascript:Matrix.js
  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制御妄想６】助けてマルチスレッド
https://qiita.com/flying_echidna/items/80b101c1a1eedb534137