LoginSignup
5
2

More than 3 years have 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;
5
2
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
5
2