LoginSignup
2

More than 1 year has passed since last update.

posted at

クォータービューを使ったレイヤー表現

20201213-195642-124.png

クォータービューで画像が何枚か重なってたりするだけで普通の 2D 描画よりなんかカッコよく見える気がします。

こういったレイヤー表現を簡単につくるためのシングルヘッダライブラリを OpenSiv3D で実装しました。

使い方とサンプルをこの記事にまとめておきます。
リポジトリはこちら:https://github.com/agehama/SivQuarterView

使い方

はじめに

まずクォータービューで文字を表示するだけのプログラムです

sample1_hello.cpp
#include <Siv3D.hpp> // OpenSiv3D v0.4.3
#include "QuarterView.hpp"

void Main()
{
    Window::Resize(400, 300);
    QuarterView quarterView(Scene::Center());

    // 解像度と平面の向きを指定して描画先となるレイヤーを取得する
    auto pLayer = quarterView.newLayer(Size(800, 600), LayerType::Y);

    while (System::Update())
    {
        quarterView.update();

        {
            auto r = pLayer->render();

            /*
            * ここで通常の2D描画を行うとクォータービュー視点に変換されて描画される
            */
            SimpleGUI::Headline(U"Hello!", Vec2::Zero());
        }

        quarterView.draw();
    }
}

QuarterView::newLayer() でレイヤーを作成してループの中でレイヤーの render() 関数を呼ぶと、そのスコープ内で行った 2D 描画がクォータービューの視点で表示されます。
20201212-174926-451.png

床と壁を描画する

次に X 平面、Y 平面、Z 平面のそれぞれに文字を描画するプログラムです

sample2_xyz_plane.cpp
#include <Siv3D.hpp> // OpenSiv3D v0.4.3
#include "QuarterView.hpp"

void Main()
{
    Window::Resize(800, 800);
    Scene::SetBackground(ColorF(0.95));
    Font font(64);

    const Color colorX(U"#f26d6d");
    const Color colorY(U"#62e762");
    const Color colorZ(U"#67acf2");

    QuarterView quarterView(Scene::Center());

    constexpr Size textureSize(300, 300);
    auto pLayerX = quarterView.newLayer(textureSize, LayerType::X);
    auto pLayerY = quarterView.newLayer(textureSize, LayerType::Y);
    auto pLayerZ = quarterView.newLayer(textureSize, LayerType::Z);

    while (System::Update())
    {
        quarterView.update();

        {
            auto r = pLayerX->render();

            font(U"X平面").drawAt(r.center(), colorX);

            RectF(50, 50).setPos(Arg::bottomRight = r.bottomRight()).draw(colorX);
            r.rect().drawFrame(10, colorX);
        }

        {
            auto r = pLayerY->render();

            font(U"Y平面").drawAt(r.center(), colorY);

            RectF(50, 50).setPos(Vec2::Zero()).draw(colorY);
            r.rect().drawFrame(10, colorY);
        }

        {
            auto r = pLayerZ->render();

            font(U"Z平面").drawAt(r.center(), colorZ);

            RectF(50, 50).setPos(Arg::bottomLeft = r.bottomLeft()).draw(colorZ);
            r.rect().drawFrame(10, colorZ);
        }

        quarterView.draw();
    }
}

render() 関数の返す LayerRegion からレイヤーの矩形情報を取得することができます。
何も設定せずにレイヤーを作って描画するとこのように原点揃えで配置されます。
20201213-171958-486.png

次にこのレイヤー配置を設定する方法について説明します。

レイヤーの配置方法

各レイヤーが配置される座標はレイヤーの Position, Elevation, AlignPos の設定値から計算されます。

[Position - 水平方向の移動量] : Vec2

レイヤーを平面と平行な軸に沿ってどれだけ移動させるかを設定します
quarterview_position_.png

QuarterLayer::setPosition() もしくは QuarterLayer::setTargetPosition() から設定します。

[Elevation - 垂直方向の移動量] : double

レイヤーを平面と垂直な軸に沿ってどれだけ移動させるかを設定します
quarterview_elevation.png

QuarterLayer::setElevation() もしくは QuarterLayer::setTargetElevation() から設定します。

[AlignPos - レイヤー揃えの基準位置] : enum class LayerAlignPos

Position で設定した座標にレイヤーのどの位置を揃えるかを指定します
quarterview_alignPos.png

各平面のデフォルト値は X 平面 -> BottomRight、Y 平面 -> TopLeft、Z 平面 -> BottomLeft に設定されています。
この値は QuarterLayer::alignPos から変更することができます。

サンプル:レイヤーを動かす

各レイヤーに図形を描画しながらレイヤーの Elevation を動かすプログラムです

sample3_shape_elevation.cpp
#include <Siv3D.hpp> // OpenSiv3D v0.4.3
#include "QuarterView.hpp"

void Main()
{
    Window::Resize(1000, 1000);
    Scene::SetBackground(Color(U"#cdd3d7"));
    Font font(20, Typeface::Heavy);

    QuarterView quarterView(Scene::Center());

    constexpr Size textureSize(150, 150);
    auto layers = Array<QuarterLayerPtr>::IndexedGenerate(9,
        [&](size_t i) { return quarterView.newLayer(textureSize, LayerType::Y, 0.0, Vec2(i % 3, i / 3) * 200.0); });

    const int margin = 100;
    const auto gridLayerSize = Point::One() * (200 * 2 + 150);
    auto pLayerFloor = quarterView.newLayer(gridLayerSize + Size(margin, margin) * 2, LayerType::Y);
    pLayerFloor->drawGroup = -1;
    pLayerFloor->setPosition(Vec2(-margin, -margin));

    quarterView.focus(*pLayerFloor);

    const auto drawLayerFrame = [&](const String& name)
    {
        Rect(textureSize).drawFrame(10, Color(240));
        const Point textPos(5, 0);
        const auto text = font(name);
        text.region().moveBy(textPos).stretched(4).draw(Color(240));
        text.draw(textPos + Vec2::One() * 1.5, Palette::Gray);
        text.draw(textPos, Palette::Black);
    };

    const Array<Polygon> shapes({
            Shape2D::Arrow(Vec2(-30, 0),Vec2(30, 0), 20, Vec2(20, 20)),
            Shape2D::Cross(30, 10),
            Shape2D::Hexagon(30),
            Shape2D::Pentagon(30),
            Shape2D::Plus(30, 10),
            Shape2D::RectBalloon(RectF(60, 40).setCenter(Vec2::Zero()),Vec2(30, 40)),
            Shape2D::Rhombus(30, 60),
            Shape2D::Stairs(Vec2::Zero(), 60, 60, 5),
            Shape2D::Star(30)
        }
    );

    const double maxElevation = 50.0;

    while (System::Update())
    {
        quarterView.update();

        for (auto [i, pLayer] : Indexed(layers))
        {
            const double elevation01 = Math::InvLerp(0.0, maxElevation, pLayer->getElevation());

            // Elevation が動いている途中でなければ一定確率で動かす
            if (!pLayer->isElevationMoving() && RandomBool(0.01))
            {
                // 目標とする Elevation を設定してその値まで 400ms かけてイージングしながら遷移する
                pLayer->setTargetElevation(0.5 < elevation01 ? 0.0 : maxElevation, 400);
            }

            auto r = pLayer->render();

            r.rect().draw(HSV(20 * i, 0.8, 0.8, elevation01));
            shapes[i].movedBy(-shapes[i].centroid() + r.rect().center()).draw(HSV(20 * i, 0.3, 0.4).lerp(Palette::White, elevation01));

            drawLayerFrame(Format(U"Layer", i));
        }

        {
            auto r = pLayerFloor->render();

            r.rect().drawFrame(4.0, Color(50));

            for (auto pLayer : layers)
            {
                RectF(pLayer->size()).moveBy(pLayer->getPosition() + Vec2(margin, margin)).draw(Color(U"#e6e8e7"));
            }
        }

        quarterView.draw();
    }
}

QuarterViewTutorial_Move.gif

サンプル:波の描画

レイヤーを使って 2 変数の関数を可視化するプログラムです

sample4_wave.cpp
#include <Siv3D.hpp> // OpenSiv3D v0.4.3
#include "QuarterView.hpp"

double WaveFunc(double x_, double z_)
{
    const double height = 30.0;
    const double width = 3.0;
    const double x = x_ * width;
    const double z = z_ * width;

    return height * (Math::Cos(x + z) + Math::Sin(Math::Cos(2 * x + 3 * z) + x - z));
}

Array<Vec2> FunctionPoints(const Rect& rect, double xBegin, double xEnd, double z, std::function<double(double, double)> f)
{
    const int divNum = 100;

    return Array<Vec2>::IndexedGenerate(divNum,
        [&](size_t i)
        {
            const double t = 1.0 * i / (divNum - 1);
            return Math::Lerp(rect.leftCenter(), rect.rightCenter(), t) + Vec2(0, f(Math::Lerp(xBegin, xEnd, t), z));
        }
    );
}

void Main()
{
    Window::Resize(1000, 1000);
    Scene::SetBackground(Color(U"#204b57"));

    QuarterView quarterView(Scene::Center());

    constexpr Size textureSize(300, 300);
    const int layerSize = 40;
    auto layersZ = Array<QuarterLayerPtr>::IndexedGenerate(layerSize,
        [&](size_t i) { return quarterView.newLayer(textureSize, LayerType::Z, i * 300.0 / (layerSize - 1)); });

    Array<QuarterLayerPtr> layersX({
        quarterView.newLayer(textureSize, LayerType::X, 0.0),
        quarterView.newLayer(textureSize, LayerType::X, 300.0)
        }
    );

    const auto getColor = [](double z) { return HSV(160 + 120 * z, 0.4, 0.7); };

    Stopwatch watch(true);
    while (System::Update())
    {
        quarterView.update();

        const double shift = 0.2 * watch.sF();
        const double xShift = shift * 0.5;
        const double zShift = shift * 0.7;

        for (auto [layerIndex, pLayer] : Indexed(layersZ))
        {
            auto r = pLayer->render();

            const double t = 1.0 * layerIndex / (layersZ.size() - 1);
            auto points = FunctionPoints(r.rect(), xShift, xShift + 1, zShift - t, [](double x, double z) { return WaveFunc(x, z); });

            Polygon(points.append(Array<Vec2>{r.rect().br(), r.rect().bl()})).draw(getColor(t));
        }

        for (auto [layerIndex, pLayer]: Indexed(layersX))
        {
            auto r = pLayer->render();

            const auto points = FunctionPoints(r.rect(), zShift - 1, zShift, xShift + layerIndex, [](double x, double z) { return WaveFunc(z, x); });

            const double bottomY = r.bottomLeft().y;
            for (size_t i = 0; i + 1 < points.size(); i++)
            {
                const Color c0(getColor(1.0 - 1.0 * i / (points.size() - 1)), 200);
                const Color c1(getColor(1.0 - 1.0 * (i + 1) / (points.size() - 1)), 200);
                const Vec2& p0 = points[i];
                const Vec2& p1 = points[i + 1];
                Quad(p0, p1, Vec2(p1.x, bottomY), Vec2(p0.x, bottomY)).draw(c0, c1, c1, c0);
            }
        }

        quarterView.draw();
    }
}

QuarterViewSample_Wave_.gif

クォータービュー機能リファレンス

利用可能な関数の一覧を以下に示します

QuarterView クラスの機能

クォータービューの更新を行う: update()

全レイヤーの更新を行います

void QuarterView::update();

※レイヤーの render() 関数よりも先に呼ばれる必要があります


クォータービューの描画を行う: draw()

各レイヤーの持つテクスチャを描画します

void QuarterView::draw();

QuarterView が持つレイヤーの中で、QuarterView::update() の後に QuarterLayer::render() が呼ばれたレイヤーのテクスチャのみがここで描画されます。

各テクスチャの描画は QuarterLayer::drawGroup の小さい順に行われます。QuarterLayer::drawGroup の値が同じレイヤー同士は elevation の値が低い順に描画されます。


クォータービューの描画を行う(一部のみ): drawPartial()

指定した QuarterLayer::drawGroup の範囲にあるレイヤーのみ描画します

void QuarterView::drawPartial(int32 beginGroupIndex, size_t drawGroupCount = -1);
  • beginGroupIndex : 描画を始める QuarterLayer::drawGroup の値
  • drawGroupCount : 描画を進める GroupIndex の個数
    • -1 を指定すると beginGroupIndex 以降にある全てのレイヤーの描画を行います

テクスチャの resolve() を実行する: resolve()

描画コマンドを全てフラッシュしてレイヤーを読み取れる状態にします

void QuarterView::resolve();

QuarterView::draw() の中で呼ばれるため通常は実行する必要はありません。
レイヤーのテクスチャを別のレイヤーに描きこむ場合などには呼ぶ必要があります。


新しいレイヤーを作成する: newLayer()

QuarterLayerPtr QuarterView::newLayer(const Size& resolution, LayerType type = LayerType::Z,
    double elevation = 0.0, const Vec2& position = Vec2::Zero())

レイヤーを削除する: erase()

void QuarterView::erase(QuarterLayerPtr eraseLayer)

軸方向を取得する: vectorX() / vectorY() / vectorZ()

クォータービューの各軸の方向ベクトルをスクリーン座標系で取得します

Vec2 QuarterView::vectorX()const;
Vec2 QuarterView::vectorY()const;
Vec2 QuarterView::vectorZ()const;

レイヤーの四角形を取得する: screenQuad()

レイヤー情報を引数に取り、そのレイヤーの描画範囲となる四角形をスクリーン座標系で取得します

Quad QuarterView::screenQuad(const QuarterLayer& layer)const;
Quad QuarterView::screenQuad(LayerType LayerType, LayerAlignPos alignType,
    const Size& layerSize, const Vec2& position, double elevation, const Vec2& scale)const;

レイヤーの座標を取得する: screenCenter() / screenPos()

レイヤーの座標をスクリーン座標系で取得します

Vec2 QuarterView::screenCenter(const QuarterLayer& layer)const;
Vec2 QuarterView::screenPos(const QuarterLayer& layer, LayerAlignPos focusPos)const;

レイヤーにカメラを合わせる: focus()

レイヤーにクォータービューの視点を設定します

void QuarterView::focus(const QuarterLayer& layer);
void QuarterView::focus(const QuarterLayer& layer, LayerAlignPos focusPos);

クォータービューの角度を設定する: setAngle()

レイヤーにクォータービューの角度を設定します

void QuarterView::setAngle(double angle);
  • angle : X 軸と Z 軸が真横から何度下に傾くか
    • (0_deg, 90_deg) の間の値を設定します
    • 初期値は 30_deg に設定されています

QuarterLayer クラスの機能

レイヤーへの描画を行う: render()

この関数を呼んでから戻り値の LayerRegion がスコープを抜けるまでの間、描画命令をレイヤーが持つテクスチャに対して行います

LayerRegion<std::tuple<ScopedRenderTarget2D, ScopedRenderStates2D, Transformer2D>>
    QuarterLayer::render(bool clearColor = true, bool transformCursor = true);
  • clearColor : 描画前にレイヤーをクリアする
    • false を指定した場合は前のフレームで描画した内容が残ります
    • 1フレームで同じレイヤーの render() を複数回呼ぶ場合は最初だけ true を指定します
  • transformCursor : カーソルをレイヤーの座標系に変換する
    • レイヤー上で SimpleGUI や図形の leftClicked() などカーソル座標を使う場合は true を指定します

ここで描画したテクスチャは、このレイヤーを持つ QuarterViewdraw() 関数が呼ばれたとき画面に描画されます。


描画を行う: renderDirectly()

この関数を呼んでから戻り値の LayerRegion がスコープを抜けるまでの間、描画命令をレイヤーの座標系に変換して行います

LayerRegion<Transformer2D>
    QuarterLayer::renderDirectly(bool transformCursor = true)const;
  • transformCursor : カーソルをレイヤーの座標系に変換する
    • レイヤー上で SimpleGUI や図形の leftClicked() などを使う場合は true を指定します

この描画は現在のレンダーターゲットに対して直接行われるため、drawGroup や elevation に関係なく renderDirectly() を呼んだ順番で画面に描画されます。


レイヤーのサイズを取得する: width() / height() / size()

レイヤーが持つテクスチャの解像度を取得します

int32 QuarterLayer::width()const;
int32 QuarterLayer::height()const;
Size  QuarterLayer::size()const;

レイヤーの背景色をセットする: setBackground()

QuarterLayer::render() がレイヤーのテクスチャをクリアする色を設定します

void QuarterLayer::setBackground(Color color);

テクスチャが resolve されたかを取得する: isResolved()

レイヤーのテクスチャが resolve() されたかを取得します

bool QuarterLayer::isResolved()const;

レイヤーの render()setBackground() が呼ばれていないときは常に true を返します。


render が呼ばれたかを取得する: isRendered()

現在のフレームでレイヤーの render() が呼ばれたかどうかを取得します

bool QuarterLayer::isRendered()const;

レイヤーの変換行列を取得する: getMat() / GetMat()

レイヤーの座標系に変換するための行列を取得します

Mat3x2 QuarterLayer::getMat()const;

static Mat3x2 QuarterLayer::GetMat(double angle1, double angle2, const Vec2& screenOrigin,
    LayerType type, LayerAlignPos alignType, const Size& textureSize,
    const Vec2& position, const Vec2& scale, double elevation);

レイヤーの水平方向の位置設定: getPosition() / setPosition() / setTargetPosition() / isPositionMoving()

getPosition() でレイヤーの現在の水平方向の位置を取得します。
setPosition() / setTargetPosition() でレイヤーの水平方向の位置を設定します。
setTargetPosition() は現在の値から設定した値まで指定のイージングを用いて滑らかに遷移します。

Vec2 QuarterLayer::getPosition()const;
void QuarterLayer::setPosition(const Vec2& newPosition, bool force = true);

void QuarterLayer::setTargetPosition(const Vec2& newPosition, int32 transitionMilliSec = 200, std::function<double(double)> transitionFunc = EaseOutCirc);
bool QuarterLayer::isPositionMoving()const;

レイヤーの垂直方向の位置設定: getElevation() / setElevation() / setTargetElevation() / isElevationMoving()

getElevation() でレイヤーの現在の垂直方向の位置を取得します。
setElevation() / setTargetElevation() でレイヤーの垂直方向の位置を設定します。
setTargetElevation() は現在の値から設定した値まで指定のイージングを用いて滑らかに遷移します。

double QuarterLayer::getElevation()const;
void QuarterLayer::setElevation(double newElevation, bool force = true);

void QuarterLayer::setTargetElevation(double newElevation, int32 transitionMilliSec = 200, std::function<double(double)> transitionFunc = EaseOutCirc);
bool QuarterLayer::isElevationMoving()const;

レイヤーのスケール設定: getScale() / setScale() / setTargetScale() / isScaleMoving()

getScale() でレイヤーの現在のスケールを取得します。
setScale() / setTargetScale() でレイヤーのスケールを設定します。
setTargetScale() は現在の値から設定した値まで指定のイージングを用いて滑らかに遷移します。

const Vec2& QuarterLayer::getScale()const;
void QuarterLayer::setScale(const Vec2& newScale, bool force = true);

void QuarterLayer::setTargetScale(const Vec2& newScale, int32 transitionMilliSec = 200, std::function<double(double)> transitionFunc = EaseOutCirc);
bool QuarterLayer::isScaleMoving()const;

レイヤーの3D位置設定: get3DPosition() / set3DPosition() / setTarget3DPosition() / is3DPositionMoving()

get3DPosition() でレイヤーの現在の3D位置を取得します。
set3DPosition() / setTarget3DPosition() でレイヤーの3D位置を設定します。
setTarget3DPosition() は現在の値から設定した値まで指定のイージングを用いて滑らかに遷移します。

Vec3 QuarterLayer::get3DPosition()const;
void QuarterLayer::set3DPosition(const Vec3& newPosition);

void QuarterLayer::setTarget3DPosition(const Vec3& newPosition, int32 transitionMilliSec = 200, std::function<double(double)> transitionFunc = EaseOutCirc);
bool QuarterLayer::is3DPositionMoving()const;

LayerRegion クラスの機能

レイヤーの矩形を取得する: rect() / rectF()

現在のレイヤーの描画範囲となる矩形を取得します

const Rect& LayerRegion::rect()const;
RectF LayerRegion::rectF()const;

レイヤー上の位置を取得する: topLeft() / topRight() / bottomLeft() / bottomRight() / center()

現在のレイヤー矩形の位置情報を取得します

Vec2 LayerRegion::topLeft()const;
Vec2 LayerRegion::topRight()const;
Vec2 LayerRegion::bottomLeft()const;
Vec2 LayerRegion::bottomRight()const;
Vec2 LayerRegion::center()const;

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
What you can do with signing up
2