0
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-31

この記事は「続・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

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

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

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

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

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

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

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

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

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

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

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

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のコードを張る。

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

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

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

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

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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0