全自動胸揺れへの道(4):そして彼女の胸は解き放たれた

いよいよ最終回。応用ちゃんに適用して胸が揺れ揺れするのだ!


…とその前に。

ふと思うことがあり、GI(LightWaveではラジオシティ有効+アンビエントオクルージョン)でライティングしてみました。
画像

おお。初期の標準ちゃんの頃に比べるとだいぶ見られるようになったと思いますが、どうすか。

ついでに2点ほどセルシェーディングとの比較画像を撮ってみたよ。
画像

画像


画像

画像


GIのほうはピンク色のリムライトを当てて、フォトショ上でさらにハイライトを加えてます。
セルシェーディングはライト増やすと影がアレなんで基本1灯です。


さて、実際のキャラモデルにゆれものを適用していくといくつか問題がありましたが、なんとかなりました。
(入れたいボーンまでの先祖ボーンにピボットローテーション(中心回転の記録)が入っているとひょっとするとアレかも知れないとか、ボーンの初期回転によってはbank回転がひっくり返るとか、ロード直後に警告が出ることがあるとか…。)
ま、小さいことは気にすまい(をぃ

今回のソースでは
  • 伸びた割合を指定の割合でSCALINGに適用(Z方向は比例、XY方向は反比例)
  • パラメータのセーブとロード
にも対応してみたよ。
これでいつでもアルバムをめくるよーに同じ揺れっぷりを見せてくれます。
もちろんコピペもできます。

またまた長いけどこれで最後だ。
[BA_pendulum.ls]
//@file   BA_pendulum.ls
//@brief  簡易ゆれもの(全部入り)
//@version 1.00
//@author  blueabyss
@version 2.3
myObj;

//定数 ---------------------

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

//ワーク -------------------

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

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

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

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

s_lastTime = 0;

//パラメータ ---------------

//ボーン原点から重りまでの長さ(0.001以上)
s_length = 0.08;

//重りの重さ(0.001以上)
s_weight = 1.0;

//伸び方向のばね係数(0以上、0の時はばねなし)
s_stretchConst = 1.0;

//ばねの最大の伸び(1.0以上)
s_maxStretch = 1.0;

//ばねの最大の縮み(0~1.0)
s_maxShrink = 1.0;

//定数ダンパー(0~1)
s_damper = 0.85;

//ローカルZ軸からの最大の傾き(0~180度)
s_maxRotAngle = 80;

//傾きに対する復元係数(0以上)
s_angleConst = 10;

//伸びに対するスケーリング(0以上)
s_stretchScale = 0.1;


create: obj {
  myObj= obj;
  setdesc("BA pendulum v1.0");
  (以下略。前回の pendulum test4 参照)
}

flags {
//  return (AFTERIK);  //なんか揺れすぎるな
  return 0;
}

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

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

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

  //質点の初期位置 = ボーンの先っちょ
  pos = <0, 0, s_length>;

  //ワールド位置に変換する
  //ピボット回転を考慮
  //ピボット回転を含める場合はローカルマトリクスの計算方法が面倒
  parent = myObj.parent;
  if( parent != nil )
    s_lastRotW = parent.getWorldRotation( s_lastTime );
  else
    s_lastRotW = <0,0,0>;
  m = rotMatrix( s_lastRotW );

  rot = myObj.getRotation( s_lastTime );
  pivot = myObj.getPivotRotation(s_lastTime);
  if( pivot != nil )
    rot += pivot;
  rm = rotMatrix( rot );
  m = mulMatrix( rm, m );
  pos = transform( m, pos );

  s_lastPosW = myObj.getWorldPosition( s_lastTime );
  s_lastPos = pos + s_lastPosW;
}

process: ma, frame, time {

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

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

  //加速度
  va = <0, -G, 0>;

  //ばね ... 元の長さからの変位量に比例する復元力を発生
  e = s_lastPos - s_lastPosW;
  len = getLength( e );
  e = normalize( e );
  d = len - s_length;
  if( d != 0 ) {
    d *= s_stretchConst / s_weight;
    va -= e * d;
  }

  //棒ばね ... ローカルZ軸からの傾きに応じた復元力を発生
  m = rotMatrix( s_lastRotW );
  rot = ma.get(ROTATION,stime);
  pivot = myObj.getPivotRotation( time );
  if( pivot != nil )
    rot += pivot;
  rm = rotMatrix( rot );
  m = mulMatrix( rm, m );
  vf = ;
  vf = normalize( vf );
  ra = dot3d(e, vf);
  ra = acos( ra ) * 180 / PI;
  if( ra>0 && s_maxRotAngle>0 && s_maxRotAngle<180 ) {
    //傾きに対する加速度
    d = rad( ra ) * s_length;
    d *= s_angleConst / s_weight;

    //加速度の向き
    v = cross3d(e,vf);    //Z軸と傾きを含む平面の法線
    v = normalize(v);
    v = cross3d(v,e);
    v = normalize( v );
    va += v * d;
  }

  //新しいボーン位置 O'
  newO = myObj.getWorldPosition( time );

  //新しい重り位置 S'
  newS = s_lastPos + s_lastVel * dt;
  newS += va * (dt * dt * 0.5);  //加速度による位置更新

  //速度更新
  s_lastVel += va * dt;
  s_lastVel *= s_damper;  //ダンパー

  //一定量以上伸び縮みしないようにする
  e = newS - newO;
  len = getLength( e );
  e = normalize( e );
  d = len / s_length;
  if( d > s_maxStretch ) {
    if( s_stretchConst==0 ) {
      //差分距離を速度に換算 v = ds/dt
      d = (len - s_length * s_maxStretch) / dt;
      d *= 2;
      s_lastVel -= e * d;
    }
    len = s_length * s_maxStretch;
    newS = newO + (e * len);
  } else if( d < s_maxShrink ) {
    len = s_length * s_maxShrink;
    newS = newO + (e * len);
  }

  //一定量以上曲がらないようにする
  parent = myObj.parent;
  if( parent != nil )
    s_lastRotW = parent.getWorldRotation( time );
  m = rotMatrix( s_lastRotW );
  rot = ma.get(ROTATION,stime);
  pivot = myObj.getPivotRotation( time );
  if( pivot != nil )
    rot += pivot;
  rm = rotMatrix( rot );
  m = mulMatrix( rm, m );
  vf = ;
  vf = normalize( vf );
  ra = dot3d(e, vf);
  ra = acos( ra ) * 180 / PI;
  if( ra > s_maxRotAngle ) {
    v = cross3d(e, vf);    //Z軸と傾きを含む平面の法線
    v = normalize(v);
    d = rad( s_maxRotAngle );
    m = rotAxisMatrix(v, d);
    e = transform( m, vf );
    newS = newO + (e * len);
  }

  s_lastPos = newS;
  s_lastPosW = newO;

  applyCurrentPosition( ma, time );
}

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

  //相対位置
  dpos = s_lastPos - s_lastPosW;

  //伸びに対するスケーリング
  if( s_stretchScale>0 ) {
    len = getLength( dpos ) * s_stretchScale / s_length;
    len = 1 + len;
    d = 1 / len;
    v = < d, d, len >;
    ma.set(SCALING, v);
  }

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

  //ボーンをS方向に向かせるためにベクトルからオイラー角を求める
  (以下略。前回の pendulum test4 参照)
}

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

//行列の乗算
mulMatrix : m1, m2 {
  (省略)
}

//任意軸 v に対する r 回転
//vは単位ベクトル、rはラジアン
rotAxisMatrix : v, r {
  (省略)
}

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

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

//ベクトルの長さを返す
getLength : v {
  (省略)
}

// Cのatan2()同等の処理
atan2 : y, x {
  (省略)
}


//設定
options {
  reqbegin("BA Pendulum");

  c_length = ctlnumber("Length", s_length);
  c_weight = ctlnumber("Weight", s_weight);
  c_stretchConst = ctlnumber("Stretch Constant", s_stretchConst);
  c_maxStretch = ctlnumber("Max Stretch Scale", s_maxStretch);
  c_maxShrink = ctlnumber("Max Shirink Scale", s_maxShrink);
  c_stretchScale = ctlnumber("Stretch Scale", s_stretchScale);
  c_maxRotAngle = ctlnumber("Max Rotate Angle", s_maxRotAngle);
  c_angleConst = ctlnumber("Angle Constant", s_angleConst);
  c_damper = ctlnumber("Damper", s_damper);

  return if ! reqpost();

  s_length = getvalue( c_length );
  if( s_length<0.001 ) s_length = 0.001;

  s_weight = getvalue( c_weight );
  if( s_weight<0.001 ) s_weight=0.001;
  if( s_weight>9999 ) s_weight=9999;

  s_stretchConst = getvalue( c_stretchConst );
  if( s_stretchConst<0 ) s_stretchConst=0;
  if( s_stretchConst>999 ) s_stretchConst=999;

  s_maxStretch  = getvalue( c_maxStretch );
  if( s_maxStretch<1 ) s_maxStretch=1;
  if( s_maxStretch>999 ) s_maxStretch=999;

  s_maxShrink    = getvalue( c_maxShrink );
  if( s_maxShrink<0 ) s_maxShrink=0;
  if( s_maxShrink>1 ) s_maxShrink=1;

  s_stretchScale = getvalue( c_stretchScale );
  if( s_stretchScale<0 ) s_stretchScale = 0;

  s_maxRotAngle = getvalue( c_maxRotAngle );
  if( s_maxRotAngle<0 ) s_maxRotAngle=0;
  if( s_maxRotAngle>180 ) s_maxRotAngle=180;

  s_angleConst = getvalue( c_angleConst );
  if( s_angleConst<0 ) s_angleConst=0;

  s_damper = getvalue( c_damper );
  if( s_damper<0 ) s_damper=0;
  if( s_damper>1 ) s_damper=1;
}

load: what,io
{
  if(what == SCENEMODE) {
    while( ! io.eof() ) {
      line = io.read();
      if( line == nil )
        break;
      items = parse(" ",line);
      s = items[1];
       if( s=="length" ) s_length = number(items[2]);
      else if( s=="weight" ) s_weight = number(items[2]);
      else if( s=="stretchConst" ) s_stretchConst = number(items[2]);
      else if( s=="maxStretch" ) s_maxStretch = number(items[2]);
      else if( s=="maxShrink" ) s_maxShrink = number(items[2]);
      else if( s=="stretchScale" ) s_stretchScale = number(items[2]);
      else if( s=="damper" ) s_damper = number(items[2]);
      else if( s=="maxRotAngle" ) s_maxRotAngle = number(items[2]);
      else if( s=="angleConst" ) s_angleConst = number(items[2]);
    }
  }
}

save: what,io
{
  if(what == SCENEMODE) {
    io.writeln( "length ", s_length );
    io.writeln( "weight ", s_weight );
    io.writeln( "stretchConst ", s_stretchConst );
    io.writeln( "maxStretch ", s_maxStretch );
    io.writeln( "maxShrink ", s_maxShrink );
    io.writeln( "stretchScale ", s_stretchScale );
    io.writeln( "damper ", s_damper );
    io.writeln( "maxRotAngle ", s_maxRotAngle );
    io.writeln( "angleConst ", s_angleConst );
  }
}

ほんでこれを適用した動画がこれ。






お約束とゆーことで、じゃっかん派手目に揺らしてみました。

前髪、ツノ(?)&後ろのハネッ毛にも適用してるよ。
髪に揺れ物が入ると圧倒的に「生きてる感」が出て魅力的になるなー。

胸の設定はこれ。
画像
前髪の設定。
画像
ツノの設定。
画像
後髪の設定。
画像

さっきも書いた通り、ボーンの角度によってはなぜかバンクがひっくり返ることがあるけど、原因が分からず。
この場合は初期状態でバンクを180度反転してキーを打てばとりあえず回避できます。

プレビューでの揺れ方とレンダリングでの揺れ方がちょっと違う(レンダリングのほうがおとなしい)ので、レンダリングしないと結果がわからない。
たぶんレンダリングはprocess()の呼ばれる回数がプレビューよりも多いんだと思う。
呼ばれる回数が多いほどダンパーによる減速が効くので。
ダンパーにデルタタイムを絡めてみるのも試したけど、なかなか思ったような感じにならなかったので、とりあえず今のままにしてます。これでもなんとか使えるので。

重力を組み込んでいる関係上、多かれ少なかれボーンは「垂れる」ので、場合によってはモデル頂点の修正が必要になります。
ボーンの初期の向きを少し上にすれば大抵は解消できると思います。

髪の毛の骨は、重力に抗おうとすると曲げ弾性を上げるか重さを相当軽くしないといけないんだけど、そうすると全然動かなくなったり硬い感じになってしまうので、場合によっては見た目を無視して骨を重力方向、つまり下向きにしてしまうほうがいいよーです。

パラメータの設定手順としては、
  • おっぱいの長さを決める(5~10cmくらいかな)
  • おっぱいの重さを決める(200~500グラムくらいではないか)
  • どのくらい伸び縮みするか決める(1.2~1.4か。伸びすぎるとかっこ悪いけど人による)
  • 曲がり弾性はとりあえず 0 にする
  • 曲がる範囲を決める(大体50度くらい)
  • ダンパーはとりあえず 1 にする
    -- ここまではささっと入力 --
  • ばね弾性を50~100辺りで調整しつつ伸び縮みの具合を決めていく
    大きめにすると弾力が増していい感じ。
  • おおざっぱに決まったら曲がり弾性を上げて、動きを調整
    小さいほうが柔らかい動きになるけど、重力に負ける。
    大きめにすると弾力が増すけど、大きすぎると暴れる。
  • 揺れが収まらない場合はダンパーを下げ、あまり動かない場合はダンパーを1.0に近づける
  • スケーリング値を 0 よりも大きくして楽しむ
という感じで進めるといいんじゃないかな。

上記の設定で垂直ジャンプも動画にしてみました。






アップのほうはカメラも上下してるのでよく分からんことになってます。
右の小さい方はカメラを固定したものです。
派手めな設定とゆーこともあっておっぱいはすごい揺れっぷりになってるけど、まあ大体思った感じに揺れてるようです。髪の毛もいい感じだ。

これでおっぱいが大きくても小さくても最小限の手間で胸揺れができるってもんです。
アニメーションさせるのが楽しくなるね。

LightWaveな兄弟もぜひ試してください。
そして絶妙な胸揺れが出たら見せてください。


当初の予定だと、複数の振り子が連なった連成振り子とコリジョンに対応してロングヘアやツインテールもOK、みたいなのもやろーと思ってたけど、時間がないのでこの辺はまた別の機会に取り組みたいと思います。

さてーあとはプロポーション&ディティールの調整かー。

ブログ気持玉

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

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

→ログインへ

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

気持玉数 : 6

なるほど(納得、参考になった、ヘー) なるほど(納得、参考になった、ヘー) なるほど(納得、参考になった、ヘー)
驚いた 驚いた
ナイス

この記事へのコメント

saru
2010年07月12日 12:41
はじめましてー
すげーっす。
でもスクリプト初心者なので
うまくうごきませんでしたぁ
できることならサンプルファイルUPしていただけないでしょうか?
2010年07月13日 23:25
や、lightwave兄弟の方ですか。
全体をアップするのはやぶさかではありませんが、兄弟からの要望が多数ありましたら対応したいと思います。

1つ重要なポイントとして、引用部分のインデント(字下げ)は見た目の関係から全角スペースに変換しています。

とりあえず、
1. 引用部分をコピーして「メモ帳」に貼り付ける
2. Ctr+Homeキーを押してカーソルを先頭行に移動し、「編集>置換」で「検索する文字列」に全角スペース、「置換後の文字列」に半角スペース2個を入力し、「すべて置換」ボタンを押して BA_pendulum.ls というファイル名で保存

という手順を試してみてください。

その後「省略」されている部分を過去の記事から補間してください。
(これも同様に全角スペースを半角スペースに置換)

胸を揺らす情熱があればきっと動くようになるはずです!
saru
2010年07月14日 12:28
はい、lightwave使いです。
おかげさまで、何とか動くようになりました。
でも何故か最初の数フレームが全く変な方向に曲がってしまい
挙動がおかしいのですが・・・・
2010年07月16日 02:26
動くようになってなによりです。

開始フレーム直後は、そこからしばらくは動かさないほうがいいです。
最初のポーズから0.5~1秒(30フレーム/秒なら15~30フレーム)そのポーズを維持してみてください。
このフレームは捨てフレームなので、レンダリング後動画を加工する際にカットしてください。

ちなみにこのアーティクルの動画では、最初のほうは動きがないのでそのまま使っています。(こんな感じで数フレーム放置すれば安定します。)
ジャンプの方は動きがあるため最初の20フレームは捨てて、ループさせるためにさらにジャンプ1回分も捨ててます。

ひょっとするとそれ以外の(ぼくが関知していない)問題かも知れないけど、その場合はオレオレ仕様ということでご容赦ください。
saru
2010年07月16日 03:53
おお
そうだったんですか!
ありがとうございます
おかげさまでおっぱいゆれまくりました!
2010年10月16日 14:40
案山子様、はじめまして。
同じLightWaveユーザーとして非常に興味深く拝見させていただきました。
数回に渡っての記事を拝見させていただき、仕込んだボーンの揺れはうまく再現できました。
ちなみにこの揺れ動作を仕込んだボーンとIKを組み合わせて長い髪の揺れに使えますか?
ゴールオブジェクトやFollowerとかいろいろ組み合わせて試してみましたが、私のスキルでは期待通りの結果が得られませんでした。
2010年10月17日 01:24
U-39さんこんにちは。
ぼくが作ったプラグインはとても初歩的なもので、残念ながら長い髪のように複数のボーンを同時に組み合わせるものには向いていません。

ぼくも今後研究しようと思っていたのですが、ボーンではなく頂点レベルの変形で、常に重力方向に下がるもので良いのであれば、ClothFXでゆれものができそうです。
サンプルの C:\LightWave_9\Scenes\ClothFX\chain1.lws をレイアウトで開き、chain:Layer1のオブジェクトプロパティの「物理演算」タブの「演算」ボタンを押してみてください。
これは鎖のサンプルですが、モデラーからchain.lwoを開いてレイヤー1の糸状になっているものをシリンダーなどに置き換え、一番上の頂点(ClothFXの影響を与えたくない頂点)を選択してセットを fix にします。
こんな感じ。
https://userdisk.webry.biglobe.ne.jp/018/956/22/N000/000/000/128724548682216119681_clothFX_hair.png
コメントでは長い文が書けないので、そのうちブログ記事にしたいと思います。
2010年10月17日 21:22
お返事ありがとうございました。
やはりFX系ですね。ボクもボーンダイナミクスでの実現を試していますが資料が少ないので思考錯誤状態です。特に重力の調整に悩んでいます。
ご紹介いただいたClothFXの事例についても試してみたいと思います。