全自動胸揺れへの道(1):LScriptで自由落下

以前、現行アプリ(ややこしいけど応用ちゃんの名前ね)でやっとくべきこととして、

  • 表情
  • 簡単なゆれもの
  • 女の子らしい指先のモデリング
    を掲げたわけですが、(1)はこれで一応終わり、(3)はまあモデリングの問題なので地道にやればいいとして、問題は(2)。

    ゆれもの(pendulum)は物理計算(physics)の比較的応用的な機能で、オープンソースな物理エンジンなんかでも大概サポートされているようだけど、使いやすいかどうかわからんのでとりあえず自分でなんとかやっとこうと思ってます。
    本格的に物理やコリジョンをやろうと思うとスクリプトではちょっと荷が重いんで最終的にはプラグインになるけど、LScriptの勉強も兼ねてある程度スクリプトでやってみます。

    しばらくはこのブログの趣旨から外れる「わけのわからないプログラムのよーなもの」が展開されますが、これも「お気楽に胸揺れする!」とゆー野望のためです。
    苦しみの先に幸せが待っているのです。


    …でLScriptだけど、実は今まで全然触ってませんでした。
    とりあえずなんたるかを知るためにユーザーマニュアルやリファレンスや標準でついてくる plugin\lscripts の中身なんかを漁ってみたけど…

    なんつーか、バージョン 9.6 にもなっていまだにアバウトな作りで、どれほどの人がまじめに使って文句を言ってそれをNewtekがどれほどまじめにフィードバックしているかが窺い知れません。
    D-STORMの日本語デベロッパーズサイトの充実のしてなさっぷりとかも、とっても不安にさせてくれます。

    ざっと見てもマニュアルの不備とか。例えば
    ・プリプロセッサの一覧がない
    ・flagsで返すことのできる値が全部書かれていない
    ・オブジェクトとオブジェクトエージェントの関係がいまいち分からない
     (モーションオプションのモディファイヤで使えるエージェントはLS_MAでええの?)
    ナド。

    あと、スクリプトとプラグインの連携もほとんどないようで、cmdSeq() や CommandInput() とかで呼び出すことはできるようだけど、戻り値を得ることができない。

    スクリプト共通の変数は globalstore() と globalrecall() で設定参照できるけど、スクリプト間の呼び出しもないみたい。
    まあスクリプト間の呼び出しはそれほど致命的ではないけど、プラグインでスクリプトのコマンドを増やせたり、変数や設定をやり取りできるだけでずいぶんと幅が広がるんだけどなぁ。

    ついでに言えば、自由な名前・型で定義できるユーザーアトリビュートのようなものがないのもちょっと。
    これにキーが打てれば、スクリプトやプラグインからそのアトリビュート値を参照して柔軟な制御ができるようになるんだけど。
    (プラグインのXPanelはキーが打てるようになってるみたいだけどまだあんまり詳しく見てないです。)

    この辺はXSIやMayaじゃずいぶんと前からできてるんで、そういった意味でもLightWaveの未来は不安だなぁ。
    かといってホビーユースでXSIやMayaに手を出すのはちょっと…だし。
    Shadeは全く触ったことないんで、この辺どうなんだろね。
    いっそのことLinuxみたいにオープンアーキテクチャにして、ユーザーにどんどん拡張してもらうようなしくみの方が庶民には支持を得られそうなんだけど。

    まあグチっててもしょうがないか。


    つーことでしばらくは絵的には地味な展開が続くんで、アプリのお宝ショットでも載せておきますよ。
    画像

    このSSはハダカアプリのプロポーション修正途中版(はづかしいんでサーフェスを水着風味にしました)。
    よく見るといろいろとアラがあったりします。
    上半身と下半身のバランスが悪いとか。
    ウェイトの調整もかなり必要そう…。
    ちなみに背景は素材集からてきとーに加工してます。3Dではないです。

    あと、全宇宙300人のアプリファンな方ならお気づきでしょうが、おっぱいがBカップになってます。
    これは胸のボーン(breast)のスケールで0.5倍にしてるだけなんで、大丈夫。いつでも戻せまっせ。
    プロポーション直す上では小さいほうが分かりやすいんで。
    (でかいとそっちに意識がいってしまって細かい所に目がいかないのよねー。)

    あと、中途半端に最近リグを入れ始めました。
    リグの入れ方は、Nullを作ってItemShapeで適当な形状にし、適当な親ボーンにアタッチして連動させたいボーンにFollowerを入れる、とゆーやり方です。
    画像
    こんな感じで1つのNullを回転させると、指のボーンを複数曲げたりできるんだ。
    ただしNull自体がマニピュレータになっているわけではないので、回す時はNullを選択してマニピュレータで回さないといけなかったりするのがLightWaveクオリティ。

    物理計算をする上ではいろいろ面倒なことがあるんだけど、ここではスクリプトで比較的簡単にできるようにするために、機能は制限して「オレが要求した機能がオレが納得する範囲で実現できればいい」とゆー「オレオレ仕様」でいきたいと思います。
    ゆれものに関しては、

    • 短い髪の毛や胸揺れをさせる
    • スクリプトレベルではコリジョンは使わない
    • 物理的な計算は見た目がそれらしければOK

    とゆー感じ。

    この辺を踏まえた上で必要そうな物理計算としては、

    • 重力による -Y 方向への落下
    • 慣性
    • ボーンの根っこを支点とした振り子運動
    • バネ

    辺りが挙げられます。

    ほんでは行ってみます。


    その1・重力落下

    最も身近で簡単な物理、自由落下。

    とりあえずこんな感じだろうと思われるものを組んでみました。
    //重力加速度による落下スクリプト by BlueAbyss
    @version 2.3
    @script motion

    //最後に計算した加速度
    lastAcc = <0, -2, 0>;

    //最後に計算した速度
    lastVel = <0, 0, 0>;

    //最後に計算した位置
    lastPos = <0, 0, 0>;

    //最後に計算した時間
    lastTime = 0;

    create: obj {
      setdesc("test Gravity0");
      resetParam();
    }

    //速度、位置をリセット
    resetParam {
      lastVel = <0, 2, 0>;  //初速 2.0m/sで打ち上げ
      lastPos = <0, 0, 0>;
    }

    process: ma, frame, time {

      //現在位置を取得
      curPos = ma.get(POSITION, time);

      dt = 0;  //デルタタイム

      //現在の時間が先頭フレームなら速度、位置をリセット
      scene = Scene();
      if( frame == scene.framestart ) {
        resetParam();
      } else {
        //デルタタイムを算出
        dt = time - lastTime;
        //何度も呼ばれる場合があるので、前回と同じか巻き戻る場合はバイパス
        if( dt<=0 )
          return;
      }

      //最後に計算した時間を更新
      lastTime = time;

      //速度を更新
      lastVel = lastVel + lastAcc * dt;

      //位置を更新
      lastPos = lastPos + lastVel * dt;

      //位置を加える
      curPos += lastPos;

      //位置を設定する
      ma.set(POSITION, curPos);
    }

    ちなみに重力加速度は9.8m/s^2だけど、ゆっくりめにするために2にしてます。

    上記を Null のモーションオプションのモディファイヤからLScriptを選択し、プロパティを開いて食わせ、実行してみると…。

    動かない。

    いろいろ悩んだけど、動かない。

    しかしもっとシンプルに
    process: ma, frame, time {
      curPos = ma.get(POSITION, time);
      curPos.y += -1.0;
      ma.set(POSITION, curPos);
    }

    とすると、とりあえず位置は変わる。

    この違いをみつけるのに1コマ費やしたよ…。
    (1コマってのは、平日、帰宅してから使える3時間程度の自由時間。普段はニュース見ながらやってたりするけど、頭使ってるとニュースがほとんど頭に残らなくて困りもの。)

    モディファイヤは1フレームに何回か呼ばれているようで、そのたびにオブジェクトの位置はフレームの「あるべき位置」にリセットされている模様。
    なので最初のソースで「バイパス」している部分でも、ポジションを更新してあげなければいけないよーです。

    つーことで、このよーにしてみれば、
        //デルタタイムを算出
        dt = time - lastTime;
        //何度も呼ばれる場合があるので、前回と同じか巻き戻る場合はバイパス
        if( dt<=0 ) {
          //計算済みの位置を反映
          curPos += lastPos;
          ma.set(POSITION, curPos);
          return;
        }

    自由落下するよーになるわけです。

    他にもモディファイヤの呼ばれる順番とか調べないといけないことがいくつかあるけど、必要に応じてやっていきます。
    (例えば首ボーンにモディファイヤにFollowerでリグを付けた場合、目にゴールで視線制御させるとちょっと怪しい動きをしたりするけど、これなんかもボーン計算の前に視線計算がされてるっぽいので、こーゆー順番を確認して対策しないとどーやっても思ったように動かなかったりします。)


    次に、拘束された落下。

    まず spine というボーンの子に breast というボーンを付け、その子にNull を付けます。
    そう、賢明な諸兄なら、これは「背中とおっぱいの骨」とゆーことに気づくでしょう。

    SSだとこんな感じです。
    画像
    ついでにこのSSでは、スクリプトをどこに付けているかも表しています。
    ほんで、Nullの形状がBallになっていますが、これはNullのプロパティから
    画像
    のよーに Item Shape を選択し、これで形状を設定してます。
    これはリグでも使ってます。

    さて、これを動かした動画はこれ。






    むーん。わけわからん動き。(右側のオレンジは非拘束のNull。)
    Nullの親ボーンの回転によって軸の向きが変わってしまったよーです。

    現在の自由落下計算はワールド座標系でやってるので、Nullを正しく動かすには、これをNullのローカル座標系に変換してやる必要があります。
    これには

    • 親のワールドローテーションを得る
    • 上記を回転行列に変換
    • 上記を逆行列に変換
    • ワールド座標での位置を上記行列でローカル座標系に変換

    つー手順を踏みます。

    ソースはこんな感じ。
    process: ma, frame, time {

      //現在位置を取得
      curPos = ma.get(POSITION, time);

      dt = 0;

      //現在の時間が先頭フレームなら速度、位置をリセット
      scene = Scene();
      if( frame == scene.framestart ) {
        LOG( "reset" );
        resetParam();
      } else {
        //デルタタイムを算出
        dt = time - lastTime;
        if( dt<=0 ) {

          //親座標系に変換
          tmpPos = lastPos;
          maParent = myObj.parent;
          if( maParent != nil ) {
            rot = maParent.getWorldRotation(time);
            mat = rotMatrix( rot );
            mat = invMatrix( mat );
            tmpPos = tranform( mat, lastPos );
          }
          curPos += tmpPos;

          ma.set(POSITION, curPos);
          return;
        }
      }

      lastTime = time;

      //速度を更新
      lastVel = lastVel + lastAcc * dt;

      //位置を更新
      lastPos = lastPos + lastVel * dt;

      //親座標系に変換
      tmpPos = lastPos;
      maParent = myObj.parent;
      if( maParent != nil ) {
        rot = maParent.getWorldRotation(time);
        mat = rotMatrix( rot );
        mat = invMatrix( mat );
        tmpPos = transform( mat, lastPos );
      }

      //位置を加え、セットする
      curPos += tmpPos;
      ma.set(POSITION, curPos);
    }

    //3×3の回転マトリクスを作成。回転オーダーはZXY
    rotMatrix : rot {
      m;

      //hpb は yxz なので注意
      sx = sin( rad( rot.y ) );
      cx = cos( rad( rot.y ) );
      sy = sin( rad( rot.x ) );
      cy = cos( rad( rot.x ) );
      sz = sin( rad( rot.z ) );
      cz = cos( rad( rot.z ) );

      m[1] = cz*cy+sz*sx*sy;
      m[2] = sz*cx;
      m[3] = -cz*sy+sz*sx*cy;
      m[4] = -sz*cy+cz*sx*sy;
      m[5] = cz*cx;
      m[6] = sz*sy+cz*sx*cy;
      m[7] = cx*sy;
      m[8] = -sx;
      m[9] = cx*cy;

      return m;
    }

    //逆行列計算
    invMatrix : mi {
      mo;

      det = mi[1] * mi[5] * mi[9] + mi[4] * mi[8] * mi[3] + mi[7] * mi[2] * mi[6]
        - mi[1] * mi[8] * mi[6] - mi[7] * mi[5] * mi[3] - mi[4] * mi[2] * mi[9];
      if( det==0 )
        error( "invMatrix" );  //逆行列はない

      det = 1.0 / det;

      mo[1] = (mi[5] * mi[9] - mi[6] * mi[8])* det;
      mo[2] = (mi[3] * mi[8] - mi[2] * mi[9])* det;
      mo[3] = (mi[2] * mi[6] - mi[3] * mi[5])* det;
      mo[4] = (mi[6] * mi[7] - mi[4] * mi[9])* det;
      mo[5] = (mi[1] * mi[9] - mi[3] * mi[7])* det;
      mo[6] = (mi[3] * mi[4] - mi[1] * mi[6])* det;
      mo[7] = (mi[4] * mi[8] - mi[5] * mi[7])* det;
      mo[8] = (mi[2] * mi[7] - mi[1] * mi[8])* det;
      mo[9] = (mi[1] * mi[5] - mi[2] * mi[4])* det;

      return mo;
    }

    //ベクトルを行列で変換
    transform : mat, vec {
      v = <0,0,0>;
      v.x = mat[1] * vec.x + mat[4] * vec.y + mat[7] * vec.z;
      v.y = mat[2] * vec.x + mat[5] * vec.y + mat[8] * vec.z;
      v.z = mat[3] * vec.x + mat[6] * vec.y + mat[9] * vec.z;
      return v;
    }


    1 は maParent.getWorldRotation()
    2 は rotMatrix()
    3 は invMatrix()
    4 は transform()
    とゆー関数でそれぞれ行ってます。1以外は今回起こしたUDF(ユーザー定義関数)。

    行列計算はここでは詳しくは書きません。
    それなりのサイトのほうでどーぞ。

    上記を適用した結果はこの動画。






    親ボーンのアニメーションに連動するのでXZ方向はもちろん、Yも連動して動いているけど、オレンジの拘束されていないNullと比較しても、落下による加速は常に下方向になっているのがわかります。

    とりあえず今回のテストで時間の変化に応じてアイテム位置を更新する方法と、マトリクスによる座標系の変換方法が確認できました。

    たぶんここと見ている大半の人が
     ( ゚Д゚)… ほがー
    となってると思うけど、理解しなくても人生には何の影響もないので安心してください。

ブログ気持玉

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

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

→ログインへ

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

気持玉数 : 0

この記事へのコメント