全自動胸揺れへの道(2):LScriptで振り子運動

前回は親ボーンにつけたNullが自由落下で落っこちていたけど、今回は親ボーンの先っちょにくっつけたまま、糸でつるした重りのよーにぶらぶらさせてみます。


例によってわけのわからん呪文を唱える前に、今回のサービスショット。
画像

今回はCカップにしてみました(をぃ
少しづつウェイトを直してます。
プラグインのほーで手いっぱいなんであんまし進んでないけど。

あと顔周りにリグを入れました。
画像
等倍表示しないと分かりづらいけど、ダイヤモンド型のNullの回転を、Followerで関連するボーンの回転や位置に振り分けてます。
Nullの位置やスケールも割り当てれば、1つのNullで最大9軸の操作ができるんだな。
だいぶ表情が付けやすくなったよ。


振り子運動の周期を決める要因としては、支点から重り(質点)の距離だけ。
自由落下でもそうだけど、重さは関係していません。
(重さは他から力が加わって運動方向が変化するとか衝突するとか、抵抗のある運動をする時に関係します。たぶん。)


今回、「重り」の位置を確認するために、 globalstore() と globalrecall() を使って、フリーなNullにコンストレインさせてみました。
スクリプトはこんな感じ。
[pendulumRef.ls]
//外部で設定されたg_lastPosをオブジェクト位置に反映 by blueabyss
@version 2.3

create: obj {
  setdesc("test PendulumRef");
}

process: ma, frame, time {
  //保険
  curPos = ma.get(POSITION, time);
  //外部で設定された位置を取得
  s = globalrecall( "g_lastPos", curPos );
  //上記はベクトルを表す「文字列」なのでベクトルに変換
  curPos = s.asVec();
  //確認用
  setdesc( curPos.asStr() );
  //位置をセット
  ma.set(POSITION, curPos);
}

で、振り子本体。

プログラムのキモとしては、
  • 重りの位置をボーンからの相対座標にしておく。ただしワールド座標系で位置だけボーンの相対。
  • 支点から重りまでの距離は restlength を使う。このためボーン限定。
    支点と重りをつなぐ「糸」は、縮み方向は制限なし、伸び方向はrestlengthを超えない。頑丈なタコ糸のよーな感じ。
  • process()で、支点と重りの位置関係から差分時間内の加速度を計算。
  • ボーンの向きを重りの方向を向くように変更。
といった感じ。
振り子の運動方程式とかは省略。ウィキペディアとかに出てます。

ソースは以下のよーな感じ。
だいぶ長くなってきてるので、前回と同じ処理部分ははしょってます。

[pendulum0.ls]
//振り子テスト by blueabyss
@version 2.3
myObj;

//定数
PI = 3.1415926;
G = 9.8;    //重力加速度

//最後に計算した速度(ワールド)
s_lastVel = <0, 0, 0>;

//最後に計算した重りの位置(向きはワールド、位置はボーンからの相対)
s_lastPos = <0, 0, 0>;

//最後のボーン回転(ワールド)
s_lastRotW = <0, 0, 0>;

//最後のボーン位置(ワールド)
s_lastPosW = <0, 0, 0>;

s_lastTime = 0;

create: obj {
  myObj= obj;
  setdesc("test Pendulum0");
  resetParam();
}

//速度、位置をリセット
resetParam {

  //myObjがBONE Object Agentsであることを確認
  if( myObj.genus != BONE )
    error( "This script is only for bone." );

  //フレーム開始時間を取得
  scene = Scene();
  s_lastTime = scene.framestart;

  //初速
  s_lastVel = <0, 0, 0>;

  //質点の初期位置 = ボーンの先っちょ
  s_lastPos = < 0, 0, myObj.restlength >;

  //Nullで位置を確認するためにワールド位置に変換する
  s_lastRotW = myObj.getWorldRotation( s_lastTime );
  m = rotMatrix( s_lastRotW );
  s_lastPos = transform( m, s_lastPos );

  s_lastPosW = myObj.getWorldPosition( s_lastTime );
  v = s_lastPos + s_lastPosW;

  //外部で参照できる変数に保存
  globalstore( "g_lastPos", v );
}

process: ma, frame, time {

  //現在の時間が先頭フレームなら速度、位置をリセット
  scene = Scene();
  if( frame == scene.framestart ) {
    resetParam();
    applyCurrentPosition( ma, time );
    s_lastTime = time;
    return;
  }

  //デルタタイムを算出
  dt = time - s_lastTime;
  if( dt<=0 ) {
    //最後に計算した値を反映
    applyCurrentPosition( ma, time );
    return;
  }

  s_lastTime = time;

  //質点をS、ボーンのローカル原点をOとする

  //ベクトルOSをワールド座標系に変換(=WOS)
  WOS = s_lastPos;
  WOS = normalize( WOS );

  //ローカル座標系変換用
  m = rotMatrix( s_lastRotW );
  len = myObj.restlength;
  curPos = myObj.getWorldPosition( time );

  _len = getLength( s_lastPos );

  if( _len >= len ) {
    //糸が張っている状態 ... 振り子運動

    //WOSとワールドDOWN(WDOWN)との為す角θを求める
    WDOWN = < 0, -1, 0 >;
    d = dot3d( WOS, WDOWN );
    seta = acos( d );

    if( seta > PI*0.5 ) {
      //θがπ/2を越える場合は、vを分解

      //法線方向の加速度
      vs = getLength( s_lastVel );
      an = vs * vs / _len;
      //y成分の加速度
      seta = PI - seta;
      any = cos( seta ) * an;

      //加速度の接線ベクトルVAを求める
      VR = cross3d( WOS, s_lastVel );
      VR = normalize( VR );
      v = WOS * -1;
      VA = cross3d( v, VR );
      VA = normalize( VA );

      s_lastVel = v * (an - any) * dt;  //法線方向
      s_lastVel += vs * VA;        //接線方向
      s_lastVel.y -= G * dt;        //
    } else {
      //加速度
      a = -(G/len) * sin( seta );

      //加速度の接線ベクトルVAを求める
      VR = cross3d( WOS, WDOWN );
      VR = normalize( VR );
      VA = cross3d( WOS, VR );
      VA = normalize( VA );

      //加速度から速度を求め、s_lastVelに加算
      s_lastVel += (a * dt) * VA;
    }

    //法線方向の速度を打ち消す
    d = dot3d( WOS, s_lastVel );
    if( d>0 )
      s_lastVel -= WOS * d;
  } else {
    //糸が張っていない状態 ... 自由落下
    s_lastVel.y -= G * dt;
  }

  //速度から位置を更新
  s_lastPos += s_lastVel * dt;

  //自由落下の場合の調整
  if( _len < len )
    s_lastPos += s_lastPosW - curPos;

  s_lastPosW = curPos;

  //OSの距離を調節する
  _len = getLength( s_lastPos );
  if( _len > len ) {
    //糸が伸びないようにする
    s_lastPos = normalize( s_lastPos );
    s_lastPos *= len;
  }

  //ローカル座標系に変換
  invm = invMatrix( m );
  s_lastPos = transform( invm, s_lastPos );

  //現在のワールド回転でワールド座標系に変換(位置は相対)
  s_lastRotW = myObj.getWorldRotation( time );
  m = rotMatrix( s_lastRotW );
  s_lastPos = transform( m, s_lastPos );

  //ワールド位置を加えて保存
  dpos = s_lastPos + curPos;
  globalstore( "g_lastPos", dpos );

  //ボーンの向きを変える
  applyCurrentPosition( ma, time );
}

//計算済みの位置を反映
applyCurrentPosition : ma, time {

  //質点Sをローカル座標系で変換
  parent = myObj.parent;
  if( parent != nil ) {
    rot = parent.getWorldRotation( time );
    m = rotMatrix( rot );
    m = invMatrix( m );
    dpos = transform( m, s_lastPos );
  } else {
    dpos = s_lastPos;
  }

  //ボーンをS方向に向かせるためにベクトルからオイラー角を求める
  dpos = normalize( dpos );
  x = dpos.x;
  y = dpos.y;
  z = dpos.z;
  ex = atan2( x, z );
  ey = -atan2( y, sqrt( z * z + x * x ) );
  e = < deg(ex), deg(ey), 0 >;

  //確認用
// setdesc( e.asStr() );

  //向きをセット
  ma.set(ROTATION, e);
}

//3×3の回転マトリクスを作成 オーダーはZXY
rotMatrix : rot {
  (省略)
}

//逆行列計算
invMatrix : mi {
  (省略)
}

//ベクトルを行列で変換
transform : mat, vec {
  (省略)
}

//ベクトルの長さを求める
getLength : v {
  len = v.x*v.x + v.y*v.y + v.z*v.z;
  if( len>=0 )
    return sqrt( len );
  return 0;
}

// Cのatan2()同等の処理
atan2 : y, x {
  if( x==0 )
  {
    if( y>0 )
      return PI * 0.5;

    if( y<0 )
      return PI * -0.5;

    return 0;
  }
  if( x>0 )
    return atan( y/x );

  if( y>=0 )
    return PI + atan( y/x );
  return - PI + atan( y/x );
}
あー長くて息が詰まるねー。

このスクリプトを、動かしたいボーン(breast)自身に付けてみたのがこの動画。






重りの初期位置によって揺れ始めます。
この動画では、親ボーン(spine)を45度寝かせて、breast自身もひねってます。
だからナナメに揺れてます。
オレンジのNullには、 pendulumRef.ls を付けて重りの位置を表しています。
水色のほうは単にbreastの子になっているNull。

揺れる勢いがだんだん弱まってる感じがするけど、これは恐らく計算で無視している誤差がけっこう大きいから。
たとえば振り子の法線方向の速度を殺しているけど、本来は厳密に計算して接線方向の速度を修正してあげないといけないと思う。
でもまあどっちにしても減衰は入れる予定だし、「オレオレ仕様」上等なのでこのままにしときます。



慣性
上記のスクリプトでは、ボーンの位置移動を考慮してません。
たとえばこの動画。






普通揺らしたらその方向に揺れ始めてほしいところだけど、揺れてません。
糸が垂直になっているので、揺れる力が生じてないのです。

ボーンの差分時間当たりの移動量を調整しながら重りに反映させてみることにします。
  //糸が張っている状態 ... 振り子運動

  //親の位置が動いたら、OSとの関連により位置を追従
  v = curPos - s_lastPosW - dpos; //移動後のボーン位置O'とS
  d = getLength( v ) - len;
  if( d>0 ) {
    v = normalize( v );
    s_lastVel += v * (d / dt);
  }

  //WOSとワールドDOWN(WDOWN)との為す角θを求める
  (以下略)

画像このコードは、ボーンの位置OがO'に動いて、O'Sの長さが糸の長さよりも長くなった時に重りSが引っ張られてS'に動いたとして、SS'の移動距離を差分時間で微分して速度に換算してます。
これで合ってるかどうかはわからないけど、それなりに動いているよーなのでよしとします。

動かしてみた動画はこんな感じ。






あと、spineの向きだけを動かしてみてもそれなりに動くでよ。






これはちょうど釣竿をキャスティングするときのよーな動きだね。
本来は投げる前にもっと寝かさないとちゃんと飛ばないけど。


今回の実装でボーンの向きを変える方法の確認と、振り子運動、慣性などが確認できました。
あとatan2風味の関数と、それを使ってベクトルの向きをオイラー角に変換する方法も実装してます。
これは最近うまく動いていない視線制御に使えるかも(でもIKの後にやりたいんでスクリプトだとだめだな。)

今回は支点と重りの距離を固定していたけど、ゴムのように伸び縮みするようにして、「動きにくさ」「元の形に戻ろうとする力」を加えていけば、全自動な胸揺れに限りなく近づきます(たぶん)。

だいたい予定している3分の2くらいまできたけど、思ったよりも確認作業が多くて当初予定していたようには進んでないなぁ。あと1~2週間ってとこか。
まあなんとか海の日までには胸揺れとプロポーション調整とウェイトの調整が終わるかなー。

ブログ気持玉

クリックして気持ちを伝えよう!

ログインしてクリックすれば、自分のブログへのリンクが付きます。

→ログインへ

なるほど(納得、参考になった、ヘー)
驚いた
面白い
ナイス
ガッツ(がんばれ!)
かわいい

気持玉数 : 0

この記事へのコメント

2010年07月02日 01:12
はじめまして。
 LS-IAでの評価時は flags{ return(AFTERIK); } でIK後に出来ると思います。 ただ、LW9.6(+DLL Plugin)ではIK設定の状態によっては評価順が不定になるので、LScriptでも同様に問題があるかもしれません(~931ではその点の問題はありません)。
 後、LScript同士、LScript対DLLの通信/データ共有については詳細ではありませんが LScript2Manual.pdf(古いので個別配布だけかも?)にあります。
 LS対DLLに関しては英語版の7.5D(update)パッケージ内のSDKからヘッダファイルを回収する必要があるのが難ですが。

 おっぱ^h^h^h揺れ物には興味津々なので応援しています、是非とも完成させて下さい(ヘ_ヘ)/
2010年07月03日 00:55
おお、情報ありがとうございます。やってみたらできました。
スクリプト同士やスクリプト・DLL間のやり取りについてはまた探してみます。