TOP > プログラミング関連 > SFML非公式翻訳トップ > 【非公式翻訳】SFML2.4チュートリアル > グラフィックスモジュール > 頂点リストで好きな形の図形を作成

[原文(公式)]

◆◆ 頂点リストで好きな形の図形を作成 ◆◆
SFML2.4 非公式日本語翻訳
まえがき
SFML には2Dオブジェクトのクラスが既にたくさんあって、ほとんどの場合はそれで充分だと思います。 複雑な形の図形を作りたいときも、既存の2Dオブジェクトを組み合わせれば簡単に実現できちゃいます。 ですが! それは、あんまり効率のいい実装とは限らないのです。

たとえば……スプライトをたくさん表示すると、すぐにグラフィックカードさんに怒られてしまいます。 つまり、draw() 関数を呼ぶ回数が多いと、カードさんに苦労をかけてしまうのですね。 なにしろ draw() 関数を呼ぶたびに OpenGL の設定をやりなおすことになりますからね。 行列とかテクスチャとか。お洋服ぬぎぬぎ。こっち見るなぁ。

しかも、たとえばスプライトの場合、1回の draw()関数で、三角形を2つ描画するだけです。 こんなもったいない使い方はグラフィックカードさんにとっては想定の範囲外なのです。 イマドキの GPU はですね、もっとですね、たくさんの三角形をですね、扱えるようになっているのですよ。 それこそ数千とか数百万とか。いつまでも子供扱いしないでよねっ!

このギャップを埋めるために、SFML には、もっと細かいレベルで描画の設定ができるような機能があります。 それが 頂点リスト です。

実は、頂点リストは SFML が用意している既存の2Dオブジェクトのクラスの内部でも使われてます。 つまり、2Dオブジェクトを作るための材料みたいなもんですね。
これでもう、draw()関数1回で三角形が2つだけ、なんてケチなことをしなくても済むかも! もっと柔軟に、アナタの好きなように2Dオブジェクトを作れるのです。
ちなみに、頂点リストを使うと図形だけじゃなく、線や点を描画することもできるのですよー。やったー!
頂点って何? どうして「リスト」なの?
「頂点」というのはですね、2Dオブジェクトの最小単位です。 一言で言うと画面上の「点」です。二次元の座標 (x, y) を持ってます。 あと、もちろん、色とか、テクスチャの座標なんかも持ってます。 そのへんの細かいお話は、あとでしますね。

頂点は1つだけでは、あんまり役に立ちません。グループにすると、目に見える図形になってきます。 頂点1つだと「点」。2つだと「線」、3つになると「三角形」、4つなら「四角形」。 こういう単純な形のことを「プリミティブ」と言います。 プリミティブを組み合わせることで、複雑な形を作っていきます。

頂点が「リスト」になる理由、わかったかな?
シンプルな頂点リスト
まずは、頂点クラス sf::Vertex を見てみましょう。
シンプルなコンテナですね。 パブリックなメンバーが3つ。関数はコンストラクタだけです。

コンストラクタがたくさんありますね。
頂点に何を求めているかによって、使い分けるとよいです。 たとえば、色やテクスチャの情報は不要だったりもしますよね? 必要な属性だけを設定できるようになってます。
// 頂点を作成
sf::Vertex vertex;

// 座標を設定
vertex.position = sf::Vector2f(10, 50);

// 色を設定
vertex.color = sf::Color::Red;

// テクスチャの座標を設定
vertex.texCoords = sf::Vector2f(100, 100);
コンストラクタで属性を設定するならこんな感じ:
sf::Vertex vertex ( 
    sf::Vector2f ( 10, 50 ), 
    sf::Color::Red, 
    sf::Vector2f ( 100, 100 )
);
じゃあ、じゃあ、さっそくプリミティブを作ろう!
覚えてるかな? プリミティブは頂点が集まってできてるんでしたね? というわけで、頂点の「リスト」が必要なのでした。

SFML には頂点をリストとしてまとめて取り扱うためのシンプルなラッパークラスがあります。 sf::VertexArray さんです。
この子は、頂点のリストが、どんな図形なのか、意味付けしてくれます。
使い方は std::vector と似てます。
// 3つの頂点で「三角形」を表すぞ!
sf::VertexArray triangle(sf::Triangles, 3);

// 三角形の頂点を設定
triangle[0].position = sf::Vector2f(10, 10);
triangle[1].position = sf::Vector2f(100, 10);
triangle[2].position = sf::Vector2f(100, 100);

// 頂点の色を設定
triangle[0].color = sf::Color::Red;
triangle[1].color = sf::Color::Blue;
triangle[2].color = sf::Color::Green;

// テクスチャの座標は今回は未使用。後で試してみるよ!
これで三角形の準備完了です。いつでも描画可能。
頂点リストを描画する方法は、SFML の他の2Dオブジェクトを描画するのと大体同じです。draw()関数を使う。
window.draw(triangle);

プリミティブの内側は、頂点の色で塗りつぶされるようになってます。 頂点ごとに違う色を設定すると、グラデーションになるのですね。

ちなみに……ちなみに、なんですけど、sf::VertexArray クラスにこだわる必要はなかったりします。 このクラスを使うと便利なんですけど、 中身は 単なる std::vector<sf::Vertex> と、プリミティブの種類(sf::PrimitiveType)です。
なので、何かもっと別のことがしたいとか、std::vector が使えない場合などは、別の方法で頂点リストを管理するとよいです。

描画するときに必要なのは、window.draw()関数の引数に 「頂点リストへのポインタ」 「頂点の数」 「プリミティブの種類」 を渡すことだけです。

std::vector を使った例:
std::vector vertices;
vertices.push_back(sf::Vertex(...));
...

window.draw(&vertices[0], vertices.size(), sf::Triangles);
配列を使った例:
sf::Vertex vertices[2] =
{
    sf::Vertex(...),
    sf::Vertex(...)
};

window.draw(vertices, 2, sf::Lines);
プリミティブの種類
ここでちょっと、お勉強の時間だよ。
頂点リストで作ることができるプリミティブの種類には、どんなのがあるのかな?

上でお話したように、作れるのはシンプルな2Dのプリミティブです。 「点」「線」「三角形」「四角形」 (ちなみに「四角形」は実際には グラフィックカードさんが内部で2つの三角形に分割します)

連結されたプリミティブもあります。
連結すると、隣り合う頂点が共有されてお得な感じです。
プリミティブはつなげて使うことが多いので、これは便利。
プリミティブの種類 文で説明 図で説明
sf::Points つながってない点のグループ。 大きさのない点です。1 ピクセル。 表示倍率や視点に関係なく 1 ピクセル。
sf::Lines つながってない線のグループ。 太さのない線です。幅 1 ピクセル。 表示倍率や視点に関係なく 1 ピクセル。
sf::LinesStrip つながっている線のグループ。 ある線の終点は、次の線の始点になります。
sf::Triangles つながってない三角形のグループ。
sf::TrianglesStrip つながっている三角形のグループ。 ある三角形の最後の辺は、次の三角形の最初の辺になります。
sf::TrianglesFan 中心の1点でつながった三角形のグループ。 最初の頂点が中心点になります。 以降、次の頂点とその次の頂点とで三角形が作られていきます。
sf::Quads つながっていない四角形のグループ。 各四角形の4頂点は、時計回りまたは反時計回りで指定されている必要があります。
テクスチャを貼る
頂点リストで作った図形にも、他の2Dオブジェクト同じようにテクスチャを貼れます。
頂点にテクスチャ座標を設定してあげてね。 テクスチャのどの点が、その頂点にマッピングされるか、の設定です。
// 頂点リストで四角形を作成
sf::VertexArray quad(sf::Quads, 4);

// 矩形を作る要領。サイズ(100x100) の矩形を 座標(10, 10) に表示。
quad[0].position = sf::Vector2f(10, 10);
quad[1].position = sf::Vector2f(110, 10);
quad[2].position = sf::Vector2f(110, 110);
quad[3].position = sf::Vector2f(10, 110);

// テクスチャ内の (0, 0)~(25, 50) のエリアを切り取って貼る。
quad[0].texCoords = sf::Vector2f(0, 0);
quad[1].texCoords = sf::Vector2f(25, 0);
quad[2].texCoords = sf::Vector2f(25, 50);
quad[3].texCoords = sf::Vector2f(0, 50);
テクスチャ座標はピクセルで設定します。sf::Sprite や sf::Shape の textureRect と同じ要領です。 0.0~1.0じゃないです。OpenGL に馴染んでる人には紛らわしいかも。
頂点リストは図形の材料。つまり、すごくシンプルなモノです。 テクスチャ座標はあるけど、テクスチャそのものは持てません。
テクスチャを貼った状態で描画するには、draw()関数を実行するときにテクスチャを指定してあげてね。
sf::VertexArray vertices;
sf::Texture texture;

...

window.draw(vertices, &texture);
上のコードは短い書き方です。
他のレンダリングステート(ブレンドモードとか、トランスフォームとか)を指定したいときには、 sf::RenderStates 構造体を使ってね。 この構造体の中にテクスチャの項目もあるので、そこにテクスチャを入れる。
sf::VertexArray vertices;
sf::Texture texture;

...

sf::RenderStates states;
states.texture = &texture;

window.draw(vertices, states);
トランスフォーム(移動、回転、拡大)
トランスフォーム(移動、回転、拡大)の指定方法も、テクスチャと同じ要領です。
頂点そのものの中にはトランスフォームの情報を入れることができないので、 draw()関数の引数にトランスフォーム情報を指定してあげてね。
sf::VertexArray vertices;
sf::Transform transform;

...

window.draw(vertices, transform);
他にもレンダリングステートが必要ならば:
sf::VertexArray vertices;
sf::Transform transform;

...

sf::RenderStates states;
states.transform = transform;

window.draw(vertices, states);
トランスフォームや sf::Transform クラスについての詳しいお話は、 「移動、回転、拡大」のチュートリアルを見てね。
SFML風の2Dオブジェクトクラスを自作する
さて、これでキミは自分で作った2Dオブジェクトに 色をつけたり、テクスチャを貼ったり、移動・回転・拡大をさせる方法がわかった。 と、なると、それを SFML の2Dクラスと同じようなスタイルに仕上げたくなりません? なりますよね。そうでしょうそうでしょう。 SFML にはそんなキミの願いを叶る2つのベースクラスがあるのですよー。 それが sf::Drawablesf::Transformable だ!
この2つは 既存の SFML の2Dオブジェクトクラスでもベースクラスとして使ってます ( sf::Spritesf::Textsf::Shape )。

sf::Drawable はインターフェイスです。純粋仮想関数が1つ宣言されてるだけです。 メンバー変数も関数もありません。
sf::Drawable を継承すると、SFML の既存の2Dオブジェクトと同じ要領で描画できるようになります。
class MyEntity : public sf::Drawable
{
private:

    virtual void draw (
        sf::RenderTarget& target, 
        sf::RenderStates states
    ) const;
};

MyEntity entity;
window.draw(entity); // 内部で entity.draw() が呼ばれます。
一応言っておくと、必ず上のようにしないといけないわけではないです。 同じような描画関数を作って、それを呼んであげても描画はできます。
でも、sf::Drawable をベースクラスにするやり方の方が一貫性があるのでオススメです。 プログラムの中で描画対象になるオブジェクトをリストに入れて管理したいときなんかにも、 sf::Drawable 型で統一できるので便利です(自分で作ったクラスも、SFML のクラスも同一の型として扱える)。

もう1つのベースクラス sf::Transformable には仮想関数がありません。 なので、この子を単に継承するだけで、SFML風の移動・回転・拡大の関数が手に入ります (setPosition()、setRotation()、move()、scale()、などなど)。
その機能自体の使い方については「移動、回転、拡大」のチュートリアルを見てね。

では、この2つのベースクラスと頂点リストを使った SFML風自家製クラスはどんな感じになるのか、サンプルをご覧下さい。 (テクスチャも貼ってみるよ!)
class MyEntity : public sf::Drawable, public sf::Transformable
{
public:


    // ……色、形、テクスチャを操作するための関数群をこのへんに書く……


private:

    virtual void draw ( sf::RenderTarget& target, sf::RenderStates states ) const
    {
        // トランスフォームを適用(呼び出し元のマトリクスへ上書き)
        // getTransform() は sf::Transformable で定義されてます
        states.transform *= getTransform();

        // テクスチャを適用
        states.texture = &m_texture;

        /*
          シェーダー(states.shader)や
          ブレンドモード(states.blendMode)も
          同じように適用できます。
        */

        // 頂点リストを描画
        target.draw ( m_vertices, states );
    }

    sf::VertexArray m_vertices;
    sf::Texture m_texture;
};
これで、SFML の既存のクラスと同じように扱うことができます。
MyEntity entity;

// 位置と回転を設定できるぞ!
entity.setPosition(10, 50);
entity.setRotation(45);

// 描画できるぞ!
window.draw(entity);
応用編その1:マップチップ
ここまでの内容を応用して、マップチップのクラスを作ってみましょう。
マップ全体が1つの頂点リストに含まれることになります。1回で描画できるので超高速!
ただし、この応用技が使えるのは、マップチップ全体を1つのテクスチャに貼れる場合だけです。 そうじゃない場合は、1つのマップチップ(=テクスチャ)ごとに1組の頂点リストが必要になります。
class TileMap : public sf::Drawable, public sf::Transformable
{
public:

    bool load ( 
        const std::string& tileset, 
        sf::Vector2u       tileSize, 
        const int*         tiles, 
        unsigned int       width, 
        unsigned int       height
    )
    {
        // タイルセット画像を読み込んでテクスチャを作成
        // (m_tileset は sf::Texture です)
        if (!m_tileset.loadFromFile(tileset)) {
            return false;
        }

        // 頂点リストの頂点数を調節。
        // マップチップの数に頂点数4を掛ける。チップは四角形だから。
        m_vertices.setPrimitiveType ( sf::Quads );
        m_vertices.resize ( width * height * 4 );

        // 各頂点を1枚のタイルマップに乗せる
        for (unsigned int i = 0; i < width; ++i) {
            for (unsigned int j = 0; j < height; ++j)
            {
                // タイル番号の現在位置を取得
                int tileNumber = tiles [ i + j * width ];

                // タイルセットの中での位置を求める。(テクスチャ座標)
                int tu = tileNumber % (m_tileset.getSize().x / tileSize.x);
                int tv = tileNumber / (m_tileset.getSize().x / tileSize.x);

                // 対応する4つの頂点へのポインタを取得
                sf::Vertex* quad = &m_vertices [ (i + j * width) * 4 ];

                // 頂点4つを設定
                quad[0].position = sf::Vector2f (    i    * tileSize.x,    j    * tileSize.y);
                quad[1].position = sf::Vector2f ( (i + 1) * tileSize.x,    j    * tileSize.y);
                quad[2].position = sf::Vector2f ( (i + 1) * tileSize.x, (j + 1) * tileSize.y);
                quad[3].position = sf::Vector2f (    i    * tileSize.x, (j + 1) * tileSize.y);

                // 頂点4つのテクスチャ座標を設定
                quad[0].texCoords = sf::Vector2f (    tu    * tileSize.x,    tv    * tileSize.y);
                quad[1].texCoords = sf::Vector2f ( (tu + 1) * tileSize.x,    tv    * tileSize.y);
                quad[2].texCoords = sf::Vector2f ( (tu + 1) * tileSize.x, (tv + 1) * tileSize.y);
                quad[3].texCoords = sf::Vector2f (    tu    * tileSize.x, (tv + 1) * tileSize.y);
            }
        }
        
        return true;
    }

private:

    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
    {
        // トランスフォームを適用
        states.transform *= getTransform();

        // テクスチャ(タイルセット)をセット
        states.texture = &m_tileset;

        // 頂点リストを描画
        target.draw(m_vertices, states);
    }

    sf::VertexArray m_vertices;
    sf::Texture m_tileset;
};
このクラスの使用例は、こんな感じです:
int main()
{
    // ウィンドウを作成
    sf::RenderWindow window(sf::VideoMode(448, 256), "Tilemap");

    // マップチップの配置を定義
    const int level[] =
    {
        0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
        0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 0, 0,
        1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5,
        0, 1, 0, 0, 2, 0, 5, 5, 5, 0, 1, 1, 1, 0,
        0, 1, 1, 0, 5, 5, 5, 0, 0, 0, 1, 1, 1, 2,
        0, 0, 1, 0, 5, 0, 2, 2, 0, 0, 1, 1, 1, 1,
        2, 0, 1, 0, 5, 0, 2, 2, 2, 0, 1, 1, 1, 1,
        0, 0, 1, 0, 5, 2, 2, 2, 0, 0, 0, 0, 1, 1,
    };

    // マップチップの配置にそってマップを生成
    TileMap map;
    if ( !map.load("maptip.png", sf::Vector2u(32, 32), level, 14, 8) ) {
        return -1;
    }

    // メインループ
    while (window.isOpen())
    {
        // イベントハンドリング
        sf::Event event;
        while (window.pollEvent(event))
        {
            if(event.type == sf::Event::Closed) {
                window.close();
            }
        }

        // マップを描画
        window.clear();
        window.draw(map);
        window.display();
    }

    return 0;
}


※ 訳注
サンプルではこんなマップチップ画像を読み込んでいます。
この配置で、左上が 0番、右下が 7番となります。

SPIERAL WIND さまの素材を使わせていただきました。
応用編その2:パーティクル
次の応用例も、きっとおなじみですね。パーティクルです。
かなりシンプルです。テクスチャなし。その他、少ないパラメータで実現可能です。
「点」つまり プリミティブの種類 "sf::Points" の使用例とも言えます。 1つ1つの点を毎フレーム変化させることで見栄えのする効果を生み出します。
class ParticleSystem : public sf::Drawable, public sf::Transformable
{
public:

    ParticleSystem ( unsigned int count ) :
        m_particles(count),
        m_vertices(sf::Points, count),
        m_lifetime(sf::seconds(3)),
        m_emitter(0, 0)
    {
    }

    void setEmitter ( sf::Vector2f position )
    {
        m_emitter = position;
    }

    void update ( sf::Time elapsed )
    {
        for (std::size_t i = 0; i < m_particles.size(); ++i)
        {
            // 各パーティクルの残り寿命を更新
            Particle& p = m_particles[i];
            p.lifetime -= elapsed;

            // if the particle is dead, respawn it
            // 寿命が切れたパーティクルをリセットして再開させる
            if (p.lifetime <= sf::Time::Zero) {
                resetParticle(i);
            }

            // パーティクルに合わせて、対応する頂点の位置を更新
            m_vertices[i].position += p.velocity * elapsed.asSeconds();

            // 残り寿命に合わせて、透明度を更新
            float ratio = p.lifetime.asSeconds() / m_lifetime.asSeconds();
            m_vertices[i].color.a = static_cast<sf::Uint8>(ratio * 255);
        }
    }

private:

    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
    {
        // トランスフォームを適用
        states.transform *= getTransform();

        // テクスチャは今回未使用
        states.texture = NULL;

        // 頂点リストを描画
        target.draw(m_vertices, states);
    }

private:

    struct Particle
    {
        sf::Vector2f velocity;
        sf::Time lifetime;
    };

    void resetParticle(std::size_t index)
    {
        // パーティクルにランダムな速度と寿命を与える
        float angle = (std::rand() % 360) * 3.14f / 180.f;
        float speed = (std::rand() % 50) + 50.f;
        m_particles[index].velocity = sf::Vector2f(std::cos(angle) * speed, std::sin(angle) * speed);
        m_particles[index].lifetime = sf::milliseconds((std::rand() % 2000) + 1000);

        // パーティクルに対応する頂点の位置をリセット
        m_vertices[index].position = m_emitter;
    }

    std::vector<Particle>       m_particles;
    sf::VertexArray             m_vertices;
    sf::Time                    m_lifetime;
    sf::Vector2f                m_emitter;
};
使ってみましょう。マウスの動きに合わせてパーティクルが散ります:
int main()
{
    // ウィンドウを作成
    sf::RenderWindow window(sf::VideoMode(512, 256), "Particles");

    // パーティクルクラスを生成
    ParticleSystem particles ( 1000 );

    // フレームごとの経過時間を計測するためのタイマー
    sf::Clock clock;

    // メインループ
    while (window.isOpen())
    {
        // イベントハンドリング
        sf::Event event;
        while (window.pollEvent(event))
        {
            if(event.type == sf::Event::Closed) {
                window.close();
            }
        }

        // マウスの位置にパーティクルが散る
        sf::Vector2i mouse = sf::Mouse::getPosition(window);
        particles.setEmitter(window.mapPixelToCoords(mouse));

        // 更新
        sf::Time elapsed = clock.restart();
        particles.update(elapsed);

        // 描画
        window.clear();
        window.draw(particles);
        window.display();
    }

    return 0;
}