もう一つのおっぱい揺れシミュレータの作り方

先日flashrodさんのところを参考におっぱいをシミュレートしてみたんだけどなんか変な感じ。同僚には「揺れてるんじゃなくてカップ数が変わってる」とか「これおっぱいじゃなくてお尻でしょう」とか言われる始末。


おかしい理由がパラメータの調整なのか、実装が糞なのか、その両方なのかよく分からないんだけど、何はともあれこの曲線がおっぱいに見えないと言う意見には頷かざるを得ない。陰でちょっと卑怯な補正しててこれだもんな。


ということで今日の午後は仕事してるふりをしながら、ちょっとおっぱいに思いを馳せてみた。


おっぱいとは何か。


YourAVHost・動ナビ等の、溢れる集合知の力を借りつつ熟考を重ね、ついに私は悟りを得た。おっぱいは「壁についた水袋」。これっす。身もふたもねぇ。


以前のばね-質点モデルの敗因、それは袋部分にだけとらわれて重要な「水」の部分を忘れたことではないか。我々がおっぱいに求めるものは表皮一枚か?否。我々がおっぱいを思い浮かべるとき、その妄想は必ずたぷたぷとした「重み」や「張り」を伴う。つまり重要なのはそこに含まれる水だったのだ。


おっぱいシミュレータの目指すところは、見る人に「中にある水を想像させる」ことにある。

その悟りに基づいたシミュレータ

http://blog.technohippy.net/Oppai.swf  [*1]

どうかな?かな?
形も動きも、こないだよりずっといいおっぱいだと思うんだけど?

と言うことで以下解説

まず、半円上にいくつかの質点を配置して、それらを袋状に繋ぐ。


で、袋の内側から水圧をかける。


水圧は袋全体にわたって同じ大きさで、力の向きは壁面と垂直。「壁面に垂直」というのは質点に繋がる辺のベクトルを合成&正規化して得ることにする。(よくわからない人は中学で何やってたんだじゃなくてパスカルの法則をググるべし)


あとは質点にかかる重力と水圧が適当にバランスとって、勝手にいい感じにたぷたぷしてくれる。やっぱおっぱいはなごむわぁ・・・。

将来の展望

3Dでも全く同じ方法でそれっぽくできるはず。半球状に質点を配置して水圧は質点に集まるベクトルの合成を正規化。あとは物理エンジン任せ。


おっぱいのサイズは半円よりも大きくするか小さくするかで、おっぱいの形は水圧の大きさでそれぞれ調整できる・・・はず。

余談

バネ−質点でうまく行かない理由だけど、上に書いた水云々は筆が滑っただけであんま意味なくて、実際は質点の分布がおかしいんじゃないかなと。ちゃんと全体を三角領域に分割してFEMすればそれなりにいけるはず・・・と思ってたら既にあった

ソース

Oppai.as (おっぱいシミュレータ本体)

package {
  import Box2D.Common.Math.b2Vec2;
  import Box2D.Dynamics.b2Body;
  import Box2D.Dynamics.Joints.b2DistanceJoint;
  import Box2D.Dynamics.Joints.b2DistanceJointDef;
  import flash.events.MouseEvent;
  import flash.geom.Point;
  import flash.ui.Mouse;
  import AppBase;

  public class Oppai extends AppBase {
    private var bodies:Array = new Array();

    public function Oppai() {
      super({useGround: false, gravity:1000.0});
    }

    override protected function initialize():void {
      var radius:int = 80;
      var nop:int = 20;
      for (var i:int = 0; i <= nop; i++) {
        var theta:Number = (i / nop) * Math.PI
        var p:Point = new Point(radius * Math.sin(theta), 200 - radius * Math.cos(theta));
        var fixed:Boolean = ([0, nop].indexOf(i) != -1);
        var mp:b2Body = createMassPoint(p, fixed);
        bodies.push(mp);
      }
      for (i = 1; i <= nop; i++) {
        var jointDef:b2DistanceJointDef = new b2DistanceJointDef();
        jointDef.body1 = bodies[i - 1];
        jointDef.body2 = bodies[i];
        jointDef.anchorPoint1 = bodies[i - 1].GetCenterPosition();
        jointDef.anchorPoint2 = bodies[i].GetCenterPosition();
        world.CreateJoint(jointDef);
      }
    }

    override protected function draw():void {
      graphics.clear();
      for (var i:int = 1; i < bodies.length - 1; i++) {
        var prevBody:b2Body = bodies[i - 1];
        var body:b2Body = bodies[i];
        var nextBody:b2Body = bodies[i + 1];
        var vec1:b2Vec2 = body.GetCenterPosition().Copy();
        vec1.Subtract(prevBody.GetCenterPosition());
        var vec2:b2Vec2 = body.GetCenterPosition().Copy();
        vec2.Subtract(nextBody.GetCenterPosition());
        vec1.Add(vec2);
        vec1.Normalize();
        vec1.Multiply(1450);
        body.ApplyForce(vec1, body.GetCenterPosition());
      }
      drawOppai();
    }

    private function drawOppai():void {
      graphics.lineStyle(1, 0xffffff, 1);
      var firstPoint:b2Vec2 = bodies[0].GetCenterPosition();
      graphics.moveTo(firstPoint.x, 0);
      graphics.lineTo(firstPoint.x, firstPoint.y);
      for (var k:int = 1; k < bodies.length - 1; k++) {
        var controlPoint:b2Vec2 = bodies[k].GetCenterPosition();
        var anchorPoint:b2Vec2 = controlPoint.Copy();
        anchorPoint.Add(bodies[k+1].GetCenterPosition());
        anchorPoint.Multiply(0.5);
        graphics.curveTo(controlPoint.x, controlPoint.y, anchorPoint.x, anchorPoint.y);
      }
      var lastPoint:b2Vec2 = bodies[bodies.length-1].GetCenterPosition();
      graphics.lineTo(lastPoint.x, lastPoint.y);
      graphics.lineTo(lastPoint.x, 1000);
    }
  }
}


AppBase.as (Box2Dで遊ぶときに共通で使えそうなところを適当にまとめたもの)

package {
  import Box2D.Collision.b2AABB;
  import Box2D.Collision.Shapes.*;
  import Box2D.Common.Math.*;
  import Box2D.Dynamics.*;
  import flash.display.Sprite;
  import flash.events.TimerEvent;
  import flash.geom.Point;
  import flash.utils.Timer;

  public class AppBase extends Sprite {
    public var world:b2World;
    public var ground:b2Body;
    protected var m_physScale:Number = 1.0;
    private var options:Object;
    private static var DEFAULT_OPTIONS:Object = {useGround:true, gravity:300.0};

    public function AppBase(opt:Object=null) {
      if (opt != null) options = opt else options = {};
      world = createWorld();
      if (getOptValue('useGround')) ground = createGround();
      initialize();
      startSimulation();
    }

    protected function getOptValue(key:String):* {
      if (options[key] != null) {
        return options[key];
      }
      else {
        return DEFAULT_OPTIONS[key];
      }
    }

    protected function initialize():void {
    }

    private function createWorld():b2World {
      var aabb:b2AABB = new b2AABB();
      aabb.minVertex.Set(-1000.0, -1000.0);
      aabb.maxVertex.Set(1000.0, 1000.0);
      var gravity:b2Vec2 = new b2Vec2(0.0, getOptValue('gravity'));
      var doSleep:Boolean = true;
      return new b2World(aabb, gravity, doSleep);
    }

    private function createGround():b2Body {
      var wallBoxDef:b2BoxDef = new b2BoxDef();
      wallBoxDef.extents.Set(680/m_physScale, 10/m_physScale);
      var wallBodyDef:b2BodyDef = new b2BodyDef();
      wallBodyDef.position.Set(0/m_physScale, 300/m_physScale);
      wallBodyDef.AddShape(wallBoxDef);
      var ground:b2Body = world.CreateBody(wallBodyDef);
      ground.GetShapeList().m_restitution = 0.5;
      return ground;
    }

    private function startSimulation(step:Number=1.0/60, iterations:int=10):void {
      var timer:Timer = new Timer(10);
      timer.addEventListener(TimerEvent.TIMER, function(event:TimerEvent):void {
        world.Step(step, iterations);
        draw();
      });
      timer.start();
    }

    protected function draw():void {
      graphics.clear();
    }

    protected function createMassPoint(point:Point, fixed:Boolean=false):b2Body {
      var boxDef:b2BoxDef = new b2BoxDef();
      boxDef.extents.Set(1, 1);
      if (!fixed) boxDef.density = 0.1;

      var bodyDef:b2BodyDef = new b2BodyDef();
      bodyDef.position.Set(point.x, point.y);
      bodyDef.AddShape(boxDef);
      return world.CreateBody(bodyDef)
    }
  }
}

*1:今はかなり更新されちゃってますけど、EnterとTabを押してシルエットにして、"<"キーでdampingを99に設定するとこのときとほぼ同じです(2/10 注記)

まちがいさがし

問題

mysql>  SELECT * FROM color_themes WHERE (key IN (1,2,3,4,5));
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual
that corresponds to your MySQL server version for the right syntax to use 
near 'key IN (1,2,3,4,5))' at line 1

答え

mysql>  SELECT * FROM color_themes WHERE (color_themes.key in (1,2,3,4,5));
+----+------+------+
| id | name | key  |
+----+------+------+
|  1 | 白   |    1 |
|  2 | 黄   |    2 |
|  3 | 赤   |    3 |
|  4 | 青   |    4 |
|  5 | 黒   |    5 |
+----+------+------+
10 rows in set (0.00 sec)

解説

keyは予約語だからテーブル名指定せずに書いちゃ駄目。

感想

テーブル作る時に叱ってくれよ。