2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[JUCE][box2D] JUCEで物理エンジンbox2Dを使いアニメーションを描く

Posted at

JUCEには物理エンジンライブラリ、box2Dが実装されているが、実際にどうやって使うの?と思っても、少なくともJUCEのリファレンスはないに等しく、サンプルも提供されておりません。

一方、box2D自体のサンプルは、OpenGLなどを使って結果を描写するというものはあっても、JUCEで結果をどう表示させるか?が見つからず、box2D未経験者としては困りました。

仕方なく、box2Dのサンプルを学び→JUCEでの実装を研究する、という手順を追いました。

Screenshot 2021-05-19 at 02.22.24.png

マウスで画面をクリックすると四角いオブジェクトを生成し、それが重力に従って落下。
他のオブジェクトとぶつかりあって、あんなことやこんなことが起こるプログラムを作ろう...

重力を表現する単純なサンプルを使います。(box2DのHello World的なサンプルです。)

JUCEでbox2Dを使用する際は、Projucerでプロジェクト生成時にbox2Dにチェックを入れる必要があります。

まずはお決まりの"MainComponent"に以下のメソッドを追加し、box2Dの物理ワールドを作ります。

void MainComponent::initBox()
{
    //重力を作ります。 (重力加速度 - 9.81)
    b2Vec2 gravity(0.0, -9.81); 
    //物理ワールド生成
    this->world = std::make_unique<b2World>(gravity);
       
    //地面を設定する
    b2BodyDef groundBodyDef;
    //地面の位置
    groundBodyDef.position.Set(0.0, -10);
    //地面を作る(world内に保管される)
    b2Body* groundBody = this->world->CreateBody(&groundBodyDef);
    
    //地面のfixtureを設定する
    b2PolygonShape groundBox;
    //横2000m 高20m (数値は倍になる)
    groundBox.SetAsBox(1000.0f, 10.f);
    //地面を作る(world内に保管される)
    groundBody->CreateFixture(&groundBox, 0.0f);// density
 
}

worldのCreateBodyで地面を生成することで、データはworldに保管されます。
これから、様々なオブジェクトを作って行きますが、全てworldに保管される形をとります。

このメソッドをコンストラクタで一度読んでください。

MainComponent::MainComponent()
{
    setSize (600, 400);

    initBox();
    
}

次に、どんどん追加していく四角いオブジェのクラスを作ります。

# include <JuceHeader.h>

class Box : public Component {
public:

    Box(float scale);
    ~Box();
    
    void init(b2World* world, Point<float> position, Point<float> dimensions);
    
    void paintBox(Graphics& g, int height);
    
    void updatePosition(int height);
    
    Point<float> getDimensions() { return this->dimensions; }
private:
    
    void paint(Graphics& g) override;
    void resized() override;
    
    float ratio = 1.0;
    b2Body* body = nullptr;
    b2Fixture* fixture = nullptr;
    Point<float>dimensions;
};

この中でbox2Dに関係する箇所は initメソッドです。

void Box::init(b2World* world, Point<float> position, Point<float> dimensions)
{
    this->dimensions = dimensions;
    
    float hX = dimensions.x / 2.0;
    float hY = dimensions.y / 2.0;
    
    //ボディを定義
    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;
    bodyDef.position.Set(position.x - hX, position.y + hY);
    
    //ボディ作る
    this->body = world->CreateBody(&bodyDef);
    
    //形を定義
    b2PolygonShape boxShape;
    boxShape.SetAsBox(hX, hY);
    
    //fixtureを定義
    b2FixtureDef fixtureDef;
    fixtureDef.shape = &boxShape;
    fixtureDef.density = 1.0f;
    fixtureDef.friction = 0.7f;
    this->fixture = body->CreateFixture(&fixtureDef);
}

これでbox2Dの設定はおしまいです。

Q

次は、どうやって結果をJUCEで表示する?です。

実は、2通り試しました。

  1. Componentの座標をコントロールし、Component自体を動かすケース
  2. Graphicsで図形を描写し、その座標をコントロールするケース

個人的に、2の方が綺麗に動いているように思いました。
しかしながら、1の方が一つ一つのオブジェクトの挙動などを制御したり、マウスイベントなどを活用したりしやすく、どちらも捨てがたいように思います。

ここでは両方紹介したいと思います。

以下はGraphicsでペイントするケース


void Box::paintBox(Graphics& g, int height)
{
    
    auto b = this->body->GetPosition();
    g.setColour(Colours::white);
    g.fillRect(b.x * scale,
               (float)height - b.y * scale,
               this->dimensions.getX() * scale,
               this->dimensions.getY() * scale);

}

注意点としては、JUCEの座標系はトップが0、ボトムに向かうにつれて値が大きくなります。
box2Dは逆で、高い位置の値が大きく、地面方面に向かって値が小さくなります。
ですので、Componentの高さheightからy座標を引くことでy座標をadjustしています。

次に、以下はComponentのBoundsを制御するケース
ペイントのケースでは面倒でやっていないのですが、こちらではbox2Dによって吐き出される角度も実装しています。


void Box::updatePosition(int height)
{
    auto b = this->body->GetPosition();

    setBounds(b.x * scale,
              (float)height - b.y * scale,
              this->dimensions.getX() * scale,
              this->dimensions.getY() * scale);
    
    float angle = this->body->GetAngle();

    Point<int> centrePos = Point<int>(getX() + getWidth()/2,
                                      getY() + getHeight()/2);
    
    setTransform(AffineTransform().translation(-centrePos.getX(), -centrePos.getY()).rotated(angle).translated(centrePos.getX(), centrePos.getY()));
    
}

角度の実装は少し工夫が必要で、オブジェクトの座標を回転の中心に持ってきた後、回転、元の位置に戻す、という工程が必要です。
その際、単純にgetX() getY()ではズレが生じ、ちゃんとオブジェクトのWidthとHeightも考慮する必要があります。(centrePosの箇所です。)

Repeat

次に、オブジェクトの生成方法を簡単に紹介します。

    //MainComponent.h
    std::vector<Box*> boxes;

//MainComponent.cpp
void MainComponent::createBox(int x, int y)
{
    Box* newBox = new Box(this->box2DRatio);
    newBox->init(this->world.get(),
                 Point<float>((float)x / this->box2DRatio,
                              (float)(getHeight() - y) / this->box2DRatio),
                 Point<float>(1, 1));
    
    this->boxes.push_back(newBox);
    
    addAndMakeVisible(newBox);
}

マウスをクリックするとオブジェクトが生成されるようにします。

    //MainComponent.h
    void mouseDown(const MouseEvent& e) override;

//MainComponent.cpp
void MainComponent::mouseDown(const MouseEvent& e)
{
    auto pos = e.getPosition();    
    createBox(pos.getX(), pos.getY());
}

描写・座標変更はここ
座標を更新する際は、物理ワールドの更新が必要です。

今回は、フレームレート60 (float timeStep = 1.0 / 60.0)、int velocityIterations = 6, int positionIterations = 2で行っています。

    //MainComponent.h
    std::unique_ptr<b2World> world;

//MainComponent.cpp
void MainComponent::paint (juce::Graphics& g)
{
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));

    g.setFont (juce::Font (16.0f));
    g.setColour (juce::Colours::white);
    g.drawText ("Hello World!", getLocalBounds(), juce::Justification::centred, true);
    
    this->world->Step(this->timeStep,
                      this->velocityIterations,
                      this->positionIterations);
    
    for(auto box : this->boxes)
    {
        //box->paintBox(g, getHeight()); //どっちか片方ね
        box->updatePosition(getHeight());
    }
}

リピート

最後にTimerを継承して、リピートしましょう。


void MainComponent::timerCallback()
{
    repaint();
}

私は startTimer(60); で行っています。

終わり

以上、雑で申し訳ないですが、ざっと書きました。
書き漏れ、不明瞭な箇所ありましたら、ご連絡いください。
可能な限りお答えします。

最後まで見ていただき、ありがとうございました。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?