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

<img width="400" alt="20201213-195642-124.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/10aef1f2-8aec-e478-62c7-c1b3838a39a6.png">

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

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

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

# 使い方

## はじめに

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

```cpp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/6830b749-4033-5cc6-250e-69e4f8eda465.png)


## 床と壁を描画する

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

```cpp: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` からレイヤーの矩形情報を取得することができます。
何も設定せずにレイヤーを作って描画するとこのように原点揃えで配置されます。
<img width="400" alt="20201213-171958-486.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/8bfd43e5-e7b5-73f9-3890-f51cf8a02529.png">

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


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

### [Position - 水平方向の移動量] : Vec2
レイヤーを平面と平行な軸に沿ってどれだけ移動させるかを設定します
![quarterview_position_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/60ad5e8d-0ddf-5ad9-e032-b994b0b3ccb3.png)

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

### [Elevation - 垂直方向の移動量] : double
レイヤーを平面と垂直な軸に沿ってどれだけ移動させるかを設定します
![quarterview_elevation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/51396529-c7c6-75a7-bdb3-5c885e9db0d4.png)


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

### [AlignPos - レイヤー揃えの基準位置] : enum class LayerAlignPos
**Position** で設定した座標にレイヤーのどの位置を揃えるかを指定します
![quarterview_alignPos.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/bfbf944d-eabb-075b-6508-eb90f6793e26.png)

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

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

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

```cpp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/a581e899-5f3c-b8a0-2af8-0902014be2c7.gif)


## サンプル：波の描画
レイヤーを使って 2 変数の関数を可視化するプログラムです

```cpp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100367/a2e69365-e10c-7890-4fd3-d8f75a1e49c2.gif)


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

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

## QuarterView クラスの機能

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

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

```cpp
void QuarterView::update();
```
※レイヤーの `render()` 関数よりも先に呼ばれる必要があります

---

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

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

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

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

---

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

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

```cpp
void QuarterView::drawPartial(int32 beginGroupIndex, size_t drawGroupCount = -1);
```

- **beginGroupIndex** : 描画を始める `QuarterLayer::drawGroup` の値
- **drawGroupCount** : 描画を進める GroupIndex の個数
    - **-1** を指定すると **beginGroupIndex** 以降にある全てのレイヤーの描画を行います

---

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

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

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

---

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


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

---

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

```cpp
void QuarterView::erase(QuarterLayerPtr eraseLayer)
```

---

#### 軸方向を取得する: vectorX() / vectorY() / vectorZ()
クォータービューの各軸の方向ベクトルをスクリーン座標系で取得します

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

---

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

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

```cpp
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()

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

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

---

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

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

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

---

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

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

```cpp
void QuarterView::setAngle(double angle);
```

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

## QuarterLayer クラスの機能

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

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

```cpp
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` がスコープを抜けるまでの間、描画命令をレイヤーの座標系に変換して行います

```cpp
LayerRegion<Transformer2D>
    QuarterLayer::renderDirectly(bool transformCursor = true)const;
```

- **transformCursor** : カーソルをレイヤーの座標系に変換する
    - レイヤー上で `SimpleGUI` や図形の `leftClicked()` などを使う場合は `true` を指定します

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

---

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

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

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

---

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

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

```cpp
void QuarterLayer::setBackground(Color color);
```

---

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

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

```cpp
bool QuarterLayer::isResolved()const;
```
レイヤーの `render()` や `setBackground()` が呼ばれていないときは常に `true` を返します。

---

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


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

```cpp
bool QuarterLayer::isRendered()const;
```

---

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

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

```cpp
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()` は現在の値から設定した値まで指定のイージングを用いて滑らかに遷移します。

```cpp
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()` は現在の値から設定した値まで指定のイージングを用いて滑らかに遷移します。

```cpp
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()` は現在の値から設定した値まで指定のイージングを用いて滑らかに遷移します。

```cpp
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()` は現在の値から設定した値まで指定のイージングを用いて滑らかに遷移します。

```cpp
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()

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

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

---

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

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

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