クォータービューで画像が何枚か重なってたりするだけで普通の 2D 描画よりなんかカッコよく見える気がします。
こういったレイヤー表現を簡単につくるためのシングルヘッダライブラリを OpenSiv3D で実装しました。
使い方とサンプルをこの記事にまとめておきます。
リポジトリはこちら:https://github.com/agehama/SivQuarterView
使い方
はじめに
まずクォータービューで文字を表示するだけのプログラムです
#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 描画がクォータービューの視点で表示されます。
床と壁を描画する
次に X 平面、Y 平面、Z 平面のそれぞれに文字を描画するプログラムです
#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
からレイヤーの矩形情報を取得することができます。
何も設定せずにレイヤーを作って描画するとこのように原点揃えで配置されます。
次にこのレイヤー配置を設定する方法について説明します。
レイヤーの配置方法
各レイヤーが配置される座標はレイヤーの Position, Elevation, AlignPos の設定値から計算されます。
[Position - 水平方向の移動量] : Vec2
レイヤーを平面と平行な軸に沿ってどれだけ移動させるかを設定します
QuarterLayer::setPosition()
もしくは QuarterLayer::setTargetPosition()
から設定します。
[Elevation - 垂直方向の移動量] : double
レイヤーを平面と垂直な軸に沿ってどれだけ移動させるかを設定します
QuarterLayer::setElevation()
もしくは QuarterLayer::setTargetElevation()
から設定します。
[AlignPos - レイヤー揃えの基準位置] : enum class LayerAlignPos
Position で設定した座標にレイヤーのどの位置を揃えるかを指定します
各平面のデフォルト値は X 平面 -> BottomRight
、Y 平面 -> TopLeft
、Z 平面 -> BottomLeft
に設定されています。
この値は QuarterLayer::alignPos
から変更することができます。
サンプル:レイヤーを動かす
各レイヤーに図形を描画しながらレイヤーの Elevation を動かすプログラムです
#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();
}
}
サンプル:波の描画
レイヤーを使って 2 変数の関数を可視化するプログラムです
#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();
}
}
クォータービュー機能リファレンス
利用可能な関数の一覧を以下に示します
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
を指定します
- レイヤー上で
ここで描画したテクスチャは、このレイヤーを持つ QuarterView
の draw()
関数が呼ばれたとき画面に描画されます。
描画を行う: 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;