Siv3Dで図形生成ツールを作った


こちらは、Siv3D Advent Calendar 2015 の5日目の記事です。

自分はSiv3Dを使って図形生成ツールを作った(作ってる)ので、これに関して便利だと感じた事や躓いた事を書いていこうと思います。
こちらはツールのデモ(Youtubeへのリンク)です。

1. EasingControllerがとても便利

これが最近Siv3Dを使ってて一番嬉しかった機能です。

動きなどの補間を行いたい時、単純にタイマーと三角関数を組み合わせて実装するとこうなります。

InterpolationUsingTimerMillisec.cpp
#include <Siv3D.hpp>

void Main()
{
    TimerMillisec timer;

    const Vec2 pos1(100, 100);
    const Vec2 pos2(500, 100);

    const unsigned T = 500;
    while (System::Update())
    {
        if (Input::KeyEnter.clicked)
        {
            timer.restart();
        }
        if (T < timer.elapsed())
        {
            timer.pause();
        }

        const double progress = 1.0*timer.elapsed() / T;
        Circle(Lerp(pos1, pos2, Sin(progress*HalfPi)), 50).draw();
    }
}

これに対してEasingControllerを使うとほとんどの処理を省略できます。

InterpolationUsingEasingController.cpp
#include <Siv3D.hpp>

void Main()
{
    EasingController<Vec2> switcher({ 100, 100 }, { 500, 100 }, Easing::Sine, 500);

    while (System::Update())
    {
        if (Input::KeyEnter.clicked)
        {
            switcher.start();
        }

        Circle(switcher.easeIn(), 50).draw();
    }
}

さらに、現時点でEasingControllerのテンプレート引数に使えるのはLerp関数が定義されている型のみですが、逆にLerp関数の定義さえあればどんな型に対しても使えるのがすごい所です。

Sugoi.cpp
#include <Siv3D.hpp>

/*
本当はライブラリの名前空間に勝手に定義するのはよくない(衝突の恐れがあるため)
今回はEasingControllerに行列型を使うために仕方なく…
*/
namespace s3d
{
    namespace Math
    {
        inline Mat3x2 Lerp(const Mat3x2& t1, const Mat3x2& t2, double f)
        {
            const float _f = static_cast<float>(f);
            return Mat3x2(
                Lerp(t1._11, t2._11, _f),Lerp(t1._12, t2._12, _f),
                Lerp(t1._21, t2._21, _f),Lerp(t1._22, t2._22, _f),
                Lerp(t1._31, t2._31, _f),Lerp(t1._32, t2._32, _f));
        }
    }
}

void Main()
{
    Window::Resize(1280, 720);
    const Mat3x2 transform1 = Mat3x2::Identity().rotate(Radians(90)).scale(1.0).translate(500, 0);
    const Mat3x2 transform2 = Mat3x2::Identity().rotate(Radians(0)).scale(3.0).translate(0, 0);

    EasingController<Mat3x2> switcher(transform1, transform2, Easing::Sine, 200);

    const Rect rect(0, 200, 800, 30);
    const Circle circle(200, 100, 50);

    while (System::Update())
    {
        if (Input::KeyEnter.clicked)
        {
            switcher.start();
        }

        Graphics2D::SetTransform(switcher.easeIn());

        rect.draw();
        circle.draw();
    }
}

2. GUIはまだ厳しい

個人的には縦に並んだボタンとテキストエリアの幅とかを揃えられないのが少し残念です。
あと他にはテキストエリアの色などに関しても結構カプセル化がきついので、もうちょっとカスタマイズできるようになるとアプリに合わせて色々変えられて良いんじゃないかなと思います。

自分でGUIを実装するのも大変ですが、とりあえずこんな感じで描画範囲を決める関数があると多少は楽じゃないかなと思います。それでも大変ですが。

GridSystem.hpp
//wholeAreaをcolumns列rows行に分割してこれをグリッドとし、
//そのグリッドのindexで表される位置から右にwidth個分の範囲のブロックを返す
inline RectF GetScope(const RectF& wholeArea, int columns, int rows, const Point& index, int width)
{
    const Vec2 block(wholeArea.w / columns, wholeArea.h / rows);
    return RectF(wholeArea.pos + block*index, Vec2(block.x*width, block.y));
}

3. 階層的な座標変換

親-子-孫のような包含関係のあるオブジェクトの座標変換は、子の変換行列に親の変換行列を掛け合わせる形で実現できます。

ChainedTransform.cpp
#include <Siv3D.hpp>

class Grandchild
{
public:

    void update()
    {
        if (Input::KeyZ.pressed)
        {
            m_transform = m_transform.rotate(Radians(-3.0));
        }
        if (Input::KeyX.pressed)
        {
            m_transform = m_transform.rotate(Radians(3.0));
        }
    }

    void draw(const Mat3x2& transform)
    {
        Graphics2D::SetTransform(m_transform*transform);
        m_rect.drawFrame();
    }

private:

    Mat3x2 m_transform = Mat3x2::Identity().scale(0.5).translate(0, 50);
    Rect m_rect = Rect(20, 50).setCenter(0, 0);
};

class Child
{
public:

    void update()
    {
        if (Input::KeyUp.pressed)
        {
            m_transform = m_transform.rotate(Radians(-3.0));
        }
        if (Input::KeyDown.pressed)
        {
            m_transform = m_transform.rotate(Radians(3.0));
        }

        m_child.update();
    }

    void draw(const Mat3x2& transform)
    {
        Graphics2D::SetTransform(m_transform*transform);
        m_rect.drawFrame();
        m_child.draw(m_transform*transform);
    }

private:

    Mat3x2 m_transform = Mat3x2::Identity().scale(0.5).translate(0, 50);
    Rect m_rect = Rect(20, 50).setCenter(0, 0);
    Grandchild m_child;
};


class Parent
{
public:

    void update()
    {
        if (Input::KeyLeft.pressed)
        {
            m_transform = m_transform.rotate(Radians(-3.0));
        }
        if (Input::KeyRight.pressed)
        {
            m_transform = m_transform.rotate(Radians(3.0));
        }

        m_child.update();
    }

    void draw(const Mat3x2& transform)
    {
        Graphics2D::SetTransform(m_transform*transform);
        m_rect.drawFrame();
        m_child.draw(m_transform*transform);
    }

private:

    Mat3x2 m_transform = Mat3x2::Identity().translate(0,50);
    Rect m_rect = Rect(20, 50).setCenter(0, 0);
    Child m_child;
};

void Main()
{
    Window::Resize(1280, 720);

    Parent parent;

    while (System::Update())
    {
        parent.update();
        parent.draw(Mat3x2::Identity().scale(3).translate(640, 360));
    }
}


明日は@GRGSIBERIAさんの記事です。よろしくお願いします。