Hatena::ブログ(Diary)

flashrod このページをアンテナに追加 RSSフィード

2008-02-02

[]おっぱい揺れシミュレータの作り方 17:31 おっぱい揺れシミュレータの作り方を含むブックマーク

先日ちょっと話題になったおっぱい揺れシミュレータだけど、こうやって作りかたを書いておけば誰かまっとうなのを作ってくれるかもしれないと期待して書いておく。

おっぱいの揺れを数理モデル化すると質点系(質点をバネ制約で繋いだもの)で充分だろう。ここでは簡単にするために、2D、APEでやってみる。3Dにするのは計算量が増えるだけで難易度が増すわけではないはず。ダイナミクスエンジンに必要なのは質点とバネ制約だけでよいので、APE以外のどんな物理シミュレーションエンジンでも、ほとんど同じようにできると思う。自前で作ってもそんなに難しくはない。

まず質点をつくる。ここではとりあえず半円上に7個の質点を作成する。質点はとりあえずAPEの場合CircleParticleでよい。衝突判定はしないので半径は何でもよい。

f:id:flashrod:20080202172701p:image

この配置は単に簡単に計算しやすいからこうしているけど、質点の数と配置は揺れのリアリティに影響するので工夫の余地が大きい。両端の点は動かない点とする。APEだとCircleParticleのコンストラクタでfixed=trueにすればよい。

つぎに全ての点の組み合わせをバネ制約で結ぶ。

f:id:flashrod:20080202172725p:image

バネの硬さも揺れの感じに大きく影響する。シミュレータを名乗るなら、各パラメータを変更できるUIがあるべきだろうが、今は面倒なのでやらない。

インタラクションは、とりあえずクリック位置に最も近い質点に適当な速度を与えてやることにする。下図は揺れている様子。

f:id:flashrod:20080202172747p:image

表示系を少し変更してみる。質点を滑らかに曲線で結んでみる。点列を滑らかな曲線で補完するのはスプラインでもなんでもいいけど、ここではベジェの制御点をすごくいい加減な方法ででっち上げてつないでみる。

f:id:flashrod:20080202172806p:image

下図は揺らしてみた様子。

f:id:flashrod:20080202172821p:image

さて次は、質点に一致するように画像を貼り付けて、質点の揺らいだ分だけ画像を歪ませる。嘘。たぶん続かない。

ソースは以下。

package {
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import org.cove.ape.APEngine;
    import org.cove.ape.CircleParticle;
    import org.cove.ape.Group;
    import org.cove.ape.SpringConstraint;
    import org.cove.ape.Vector;
    public class MassSystem extends Sprite {
        private var modelLayer:Sprite = new Sprite();
        private var surfaceLayer:Sprite = new Sprite();
        private var circles:Array = [];
        private var springs:Array = [];
        public function MassSystem() {
            addChild(modelLayer);
            modelLayer.visible = false;
            addChild(surfaceLayer);
            surfaceLayer.visible = true;
            APEngine.init(1/3);
            APEngine.container = modelLayer;
            APEngine.addMasslessForce(new Vector(0, 1));
            var gr:Group = new Group();
            for (var i:int = 0; i <= 6; ++i) {
                var t:Number = Math.PI * (i/6);
                var cx:Number = 640/2 + 100 * Math.cos(t);
                var cy:Number = 480/2 - 100 * Math.sin(t);
                var c:CircleParticle = new CircleParticle(cx, cy, 8, (i==0 || i==6));
                circles.push(c);
                gr.addParticle(c);
            }
            for (i = 0; i < circles.length-1; ++i) {
                for (var j:int = i + 1; j < circles.length; ++j) {
                    var s:SpringConstraint = new SpringConstraint(circles[i], circles[j], 0.5);
                    springs.push(s);
                    gr.addConstraint(s);
                }
            }
            APEngine.addGroup(gr);
            addEventListener(Event.ENTER_FRAME, enterFrame);
            stage.addEventListener(MouseEvent.CLICK, click);
        }
        private function enterFrame(e:Event):void {
            APEngine.step();
            if (modelLayer.visible) {
                APEngine.paint();
            }
            if (surfaceLayer.visible) {
                paintSurface();
            }
        }
        private function paintSurface():void {
            var a:Array = [];
            var b:Array = [];
            for (var i:int = 0; i < circles.length-2; ++i) {
                var v1:Vector = circles[i].position;
                var v2:Vector = circles[i+1].position;
                var v3:Vector = circles[i+2].position;
                var c:Array = ctrl(v1, v2, v3);
                var c1:Vector = c[0];
                var c2:Vector = c[1];
                a.push(v2);
                b.push(c1);
            }
            a.push(v3);
            b.push(c2);
            var v0:Vector = circles[0].position;
            with (surfaceLayer.graphics) {
                clear();
                lineStyle(4, 0x000000);
                moveTo(v0.x, v0.y);
                for (i = 0; i < a.length; ++i) {
                    curveTo(b[i].x, b[i].y, a[i].x, a[i].y);
                }
            }
        }
        private function ctrl(p1:Vector, p2:Vector, p3:Vector):Array {
            var m1:Vector = new Vector((p1.x + p2.x)/2, (p1.y + p2.y)/2);
            var m2:Vector = new Vector((p2.x + p3.x)/2, (p2.y + p3.y)/2);
            var t1:Vector = p2.plus(p2.minus(m1));
            var t2:Vector = p2.plus(p2.minus(m2));
            var c1:Vector = new Vector((m1.x + t2.x)/2, (m1.y + t2.y)/2);
            var c2:Vector = new Vector((m2.x + t1.x)/2, (m2.y + t1.y)/2);
            return [c1, c2];
        }
        private function search(v:Vector):CircleParticle {
            var minc:CircleParticle;
            var min:Number = Number.MAX_VALUE;
            for (var i:int = 1; i < circles.length-1; ++i) {
                var c:CircleParticle = circles[i];
                var d:Number = c.position.distance(v);
                if (d < min) {
                    min = d;
                    minc = c;
                }
            }
            return minc;
        }
        private function click(e:MouseEvent):void {
            var v:Vector = new Vector(mouseX, mouseY);
            var c:CircleParticle = search(v);
            if (c != null) {
                var w:Vector = c.position.minus(v).normalize().mult(20);
                c.velocity = w;
            }
        }
    }
}