「がんばれない」けど「がんばりたい」

ITエンジニアの仕事のこと。AI、機械学習、ディープラーニング。地頭力。車のこと。

Spring Force|Nature of Code

バネforceについて

バネの公式はHooke’s lawで定義されている。
F = -k x X;

kは定数。x は バネが伸びていない状態(rest length)から、伸ばしたり/縮めたりした時のバネの長さとの差。公式にマイナスが付いている理由はバネの片方の端が天井に付けられて固定されているとした時に、バネに加わる力が伸びている時には天井方向に力が働く=天井の起点(origin)から伸びる方向を+とするとマイナス方向。縮めた場合はバネは伸びようとする力が働く。つまりcurrent length - rest length = xの値がマイナス=縮んでいる場合はプラス方向。xの値がプラス=伸びている場合はマイナス方向の力が発生する為です。

バネを実現するには、オブジェクトに対してHooke'sの公式で求めた力Fを反映すればいい。これをコードにしていこう。おさらいになるが力Fはベクトルである。そのため力Fの大きさと方向を求める必要があります。※これも今までどおり。上図からすぐに分かるものは、origin、restLength、公式内の定数k、そしてx。このうちxって何だろう? xの値は上述の式の通りでcurrentLengthとrestLengthの差になる。lengthはベクトルの大きさですので、xを求めるには、まずcurrentLengthを求める必要があります。それには、

  1. locationからoriginまでの方向ベクトルを求める。 ofVec3f dir = location - origin;
  2. 1.で求めたベクトルの長さ=currentLengthなので、float currentLength = dir.length();
  3. x = currentLength - restLength;

で表す事ができる。起点originを画面真ん中の一番上にし、バネに何も力が加わっていない状態の長さrestLengthを200。そして重りはバネの先っぽに付いているのでlocationを(0,200,0)として、バネ定数は適当に0.1としてみました。ここまでをコードで表すと。。

//重りオブジェクト
Bob b;
b.location.set(ofGetWidth()*0.5, 200, 0);

//起点
ofVec3f origin.set(ofGetWidth()*0.5, 0, 0);

//バネ通常時の長さ
float restLength = 200.0;

//バネ定数
k = 0.1;

ofVec3f dir = b.location - origin;
float currentLength = dir.length();
float x = currentLength - restLength;

次に力Fを求めて行く。Hooke'sの公式よりFは、x * -kになるので、上記コード内でxもkも求められているから値は算出できる。がしかし!力Fはベクトルなのです。ベクトルという事は何度も言う通り、大きさと方向を持っていないといけない。大きさは-k*xでOK。では方向は? 実は既に計算ズミ。dir = b.location - origin。重りを引っ張ってバネが引っ張られている時はorigin方向だし、バネを縮めている時の力の方向は重り方向になります。ここで、またおさらい!!方向は大きさを持たないようにしないと、力Fに大きく影響を及ぼしてしまうので正規化しておく。だって、力Fの大きさは既に x * -k と決まっているから、この大きさに方向を反映してベクトルにするにあたって、方向ベクトルに大きさが存在していたら、そもそもの力の大きさに対して更に掛け算してしまって、結果的に想像してた通りにはならない事が多いです。それを踏まえて、コードにしてみました。

void testApp::update(){
    for (int i = 0; i < NUM_BOB; i++)
    {
        Bob* b = &bobs[i];
        float k = 0.1;

        ofVec3f dir = b->location - b->origin;
        float currentLength = dir.length();

        dir.normalize();

        
        float x = currentLength - b->restLength;
        dir *= (-1) * k * x;

        b->applyForce(dir);
        b->update();
    }
}

うん、だいたい出来た!と思って、実行してみてみると…スプリングが伸び縮みしていない。。答えは、初期値。結果的に重りオブジェクトに対して力Fをapplyしていますが、その大きさが0になっています。何故かと言うとrestLength=200としていて、更にlocaitionも(0,200,0)となっているので、float x を求めるところで、200 - 200 = 0になってしまい、ずーっと力の大きさが0になってしまっているから。 これを避けるには初期値としてrestLengthもしくは重りの位置を200でない値にしておけばいいです。

バネの力をクラス化して整理したコードを以下に。

#define NUM_OBJECTS 100


class Bob
{
public:
    //Bobの位置
    ofVec3f location;
    
    //加速度
    ofVec3f acceleration;
    
    //速度
    ofVec3f velocity;
    
    //質量
    float mass;
    
    /**
     *  applyForce
     */
    void applyForce(ofVec3f force)
    {
        ofVec3f f = force/mass;
        acceleration+=f;
    }
    
    
    /**
     *  初期化
     */
    void setup()
    {
        //Const
        //---------------------
        //質量
        mass = ofRandom(1.0, 10);
        
        //update
        //---------------------
        acceleration.set(0, 0, 0);
        velocity.set(0,0,0);
    }
    
    void update()
    {
        //standard
        //---------------------
        velocity += acceleration;
        location += velocity;
        
        acceleration.set(0, 0, 0);
    }
    
    void draw()
    {
        //draw anch
        int saturation = ofMap(location.z, -100, 100, 50, 255);
        ofSetColor(saturation);
        
        ofCircle(location.x, location.y, 10);
    }
};


class Spring
{
public:

    //起点
    ofVec3f anchor;

    //バネが伸縮していない(力がかかっていない)時の長さ
    float restLength;

    //バネ定数
    float k;
    
    //起点の位置で描画用
    ofRectangle rectangle;
    
    void setup()
    {
        //restLength
        restLength = ofRandom(200, 300);
        
        //固定位置
        float ax = ofRandom(0, ofGetWidth());
        float ay = ofRandom(0, ofGetHeight()-300);
        float az = ofRandom(-100, 100);
        anchor.set(ax, ay, az);
        
        k = 0.1;
    }
    
    ofVec3f connect(Bob b)
    {
        //currentLength
        ofVec3f dir = b.location - anchor;
        float currentLength = dir.length();

        //culculate force
        dir.normalize();
        float strech = currentLength - restLength;
        
        return dir * (-1) * k * strech;
    }
    
    void drawAnchor()
    {
        rectangle.setFromCenter(anchor.x, anchor.y, 5, 5);
        ofRect(rectangle);
    }
    
    void draw(Bob b)
    {
        drawAnchor();
        ofLine(anchor.x,anchor.y, b.location.x, b.location.y);
    }
};


//取りあえず
//---------------------
vector<Bob> bobs;
vector<Spring> springs;


//--------------------------------------------------------------
void testApp::setup(){
    
    ofSetFrameRate(60);
    ofEnableSmoothing();
    ofSetVerticalSync(true);
    ofBackground(0);

    ofEnableBlendMode(OF_BLENDMODE_ADD);
    
    bobs.resize(NUM_OBJECTS);
    springs.resize(NUM_OBJECTS);
    for (int i = 0; i < NUM_OBJECTS; i++)
    {
        Spring* s = &springs[i];
        s->setup();
        
        Bob* b = &bobs[i];
        b->setup();
        b->location.x = s->anchor.x;
        b->location.y = s->anchor.y + s->restLength + ofRandom(30, 200);
        b->location.z = s->anchor.z;
    }
}

//--------------------------------------------------------------
void testApp::update(){
    for (int i = 0; i < NUM_OBJECTS; i++)
    {
        Bob* b = &bobs[i];
        Spring* s = &springs[i];

        ofVec3f springForce = s->connect(*b);
        b->applyForce(springForce);
        

        b->update();
    }
}

//--------------------------------------------------------------
void testApp::draw(){
    
    for (int i = 0; i < NUM_OBJECTS; i++)
    {
        Bob* b = &bobs[i];
        b->draw();
        
        Spring* s = &springs[i];
        s->draw(*b);
    }
}