Siv3Dで4次元空間を実装したい！！

こちらは[Siv3D Advent Calendar 2023](https://qiita.com/advent-calendar/2023/siv3d)の21日目の記事です．

# はじめに
皆さんは4次元をご存じですか？
今回は最近私が興味を持った4次元超立方体の描画をSiv3Dを用いて実装してみました．

# 次元とは
次元とは空間の広がりのことです．
もっと直感的に言えば，ある質点の座標を表す際にいくつの変数が必要になるか，と言い変えることもできます．
## 0次元
0次元は点です．
質点のみが存在し，その質点は空間内を移動することができません．

## 1次元
質点が移動できる方向が一方向あります．
そのため，1次元は直線となります．
算数や数学で数直線を使ったことがありますよね．数直線は1次元であると言えます．

## 2次元
ここからはよく聞く単語になりますね．アニメなどで聞くことがあると思います．
1次元に比べて移動できる方向がさらに増え，平面となります．
縦・横に空間が広がっており，それは二つの軸で表されます．
よく$xy$平面で用いられます．

## 3次元
3次元は普段私たちが暮らしている世界です．
$xyz$空間で表されることが多く，立体的な空間となります．
<img width=25%><img width="50%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558342/b30f5391-aa17-9bd7-1e4f-c702b7c7a827.png">

## 4次元
ここで本題の4次元に突入します．
まず，4次元は数学と物理の両方に登場しますが，それらは異なったものです．
それらの違いについて簡単に解説します．

### 物理学における4次元
物理学に登場する4次元は，ミンコフスキー空間と呼ばれます．3次元空間に加える新たな軸として時間軸を加えて考えられています．
一般的に4次元と言うとこちらを想像する人が多いかと思います．

### 数学における4次元
数学に登場する4次元とは，4次元ユークリッド空間のことです．3次元までの$x, y, z$軸はそれぞれの軸が互いに直交しています．これはみなさんも直感的にわかると思います．
4次元には，この$x, y, z$軸全てに直交する新たな軸である$w$軸を追加します．
<img width=25%><img width="50%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558342/901c637e-01f1-14b6-9fa7-b9d550d2dbf3.png">

日頃3次元に暮らしている私達では簡単に想像することはできませんが，そういうものがあると考えます．
今回の記事で取り上げるのはこちらの4次元です．

# 4次元超立方体
4次元超立方体とは，正八胞体とも呼ばれるものです．
立方体が正方形を$z$軸方向に広げたものであるように，立方体を新たに追加した$w$軸方向に広げたものです．
各次元における超立方体の特徴をまとめたものを以下に示します．
|次元数|名前|頂点数|辺の数|面の数|胞の数|
|--|--|--|--|--|--|
|0|質点|1|
|1|線分|2|1|
|2|正方形|4|4|1|
|3|立方体|8|12|6|1|
|4|4次元超立方体|16|32|24|8|

4次元超立方体がどのような見た目になるのかは実装後のお楽しみです．

# 投影
3次元の立体を普段私たちはスマホやパソコンの画面で見ています．しかしこれは本来できることではありません．工夫する必要があります．
その方法として，投影法があります．
ある立体に光を当て，それをスクリーンに映すことで3次元の物体を2次元の平面上に表すことができます．
投影には二種類あり，透視投影と平行投影があります．

## 透視投影
透視投影は日頃私たちが目にしている世界です．奥行きがある世界です．
ある立方体を正面から見た際，以下の図のように遠近法により奥の正方形も見えますよね．その世界です．
<img width=25%><img width="50%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558342/bc4ea5c4-5fac-d64e-9e17-c8c3b7cf3b5c.png">


## 平行投影
こちらは透視投影と異なり，遠近法の無い世界です．立方体を正面から見ても手前の正方形と奥の正方形がぴったりと重なることでただの正方形にしか見えません．
<img width=25%><img width="50%" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558342/b6fdeded-4033-410a-516e-1ab60ed1d315.png">

# 実装
ここから実際に実装していきます．
Siv3Dは0.6.12を使用しています．

## 構造体の定義
4次元超立方体を表す構造体である`Octachoron`を定義しました．
```cpp
struct Octachoron {
	union {
		Vec4 center;

		struct {
			double x;
			double y;
			double z;
			double w;
		};
	};
	union {
		Vec4 size;

		struct {
			double width;
			double height;
			double depth;
			double tetra_width;
		};
	};
	Vec4 rotation_r{0, 0, 0, 1};
	Vec4 rotation_l{0, 0, 0, 1};

	Octachoron() = default;
	constexpr Octachoron(const Octachoron&) = default;
	constexpr Octachoron(Octachoron&&) = default;
	constexpr Octachoron(const Vec4& _center, const Vec4& _size) : center(_center), size(_size) {}
	constexpr Octachoron(Vec4&& _center, const Vec4& _size) : center(_center), size(_size) {}
	constexpr Octachoron(const Vec4& _center, Vec4&& _size) : center(_center), size(_size) {}
	constexpr Octachoron(Vec4&& _center, Vec4&& _size) : center(_center), size(_size) {}
	constexpr Octachoron(const Vec4& _center, double w, double h, double d, double t) : center(_center), width(w), height(h), depth(d), tetra_width(t) {}
	constexpr Octachoron(Vec4&& _center, double w, double h, double d, double t) : center(_center), width(w), height(h), depth(d), tetra_width(t) {}
	constexpr Octachoron(const Vec4& _center, double w) : center(_center), width(w), height(w), depth(w), tetra_width(w) {}
	constexpr Octachoron(Vec4&& _center, double w) : center(_center), width(w), height(w), depth(w), tetra_width(w) {}
	constexpr Octachoron(double _x, double _y, double _z, double _w, const Vec4& _size) : x(_x), y(_y), z(_z), w(_w), size(_size) {}
	constexpr Octachoron(double _x, double _y, double _z, double _w, Vec4&& _size) : x(_x), y(_y), z(_z), w(_w), size(_size) {}
	constexpr Octachoron(double _x, double _y, double _z, double _w, double _width, double _height, double _depth, double _tetra) : x(_x), y(_y), z(_z), w(_w), width(_width), height(_height), depth(_depth), tetra_width(_tetra) {}

	constexpr Octachoron& setPos(double _x, double _y, double _z, double _w) noexcept;
	constexpr Octachoron& setPos(Vec4 _center) noexcept;
	constexpr Octachoron& setSize(double _width, double _height, double _depth, double _tetra_width) noexcept;
	constexpr Octachoron& setSize(Vec4 _size) noexcept;
	Octachoron& setRotation(Vec4 vl, Vec4 vr);

	constexpr Octachoron movedBy(Vec4 v) const;
	constexpr Octachoron movedBy(double _x, double _y, double _z, double _w) const;
	constexpr Octachoron scaled(double m) const;
	constexpr Octachoron scaled(Vec4 v) const;
	constexpr Octachoron scaled(double _x, double _y, double _z, double _w) const;

	void drawFrame() const {
		this->parallelProjection();
	}
	void drawFrame(double f) const {
		this->perspectiveProjection(f);
	}


private:
	void parallelProjection() const;
	void perspectiveProjection(double f) const;

	using pii = std::pair<int, int>;
	constexpr static std::array<std::pair<int, int>, 32> EdgesIndex = {
		pii{0, 1},   pii{0, 2},   pii{0, 4},
		pii{0, 8},   pii{1, 3},   pii{1, 5},
		pii{1, 9},   pii{2, 3},   pii{2, 6},
		pii{2, 10},  pii{3, 7},   pii{3, 11},
		pii{4, 5},   pii{4, 6},   pii{4, 12},
		pii{5, 7},   pii{5, 13},  pii{6, 7},
		pii{6, 14},  pii{7, 15},  pii{8, 9},
		pii{8, 10},  pii{8, 12},  pii{9, 11},
		pii{9, 13},  pii{10, 11}, pii{10, 14},
		pii{11, 15}, pii{12, 13}, pii{12, 14},
		pii{13, 15}, pii{14, 15}
	};
};
```

ついでに，必要な関数も定義しました．
```cpp
static Vec4 EulerToVec4(double roll, double pitch, double yaw) {
	double q0, q1, q2, q3;
	double cosRoll = Math::Cos(roll / 2.0);
	double sinRoll = Math::Sin(roll / 2.0);
	double cosPitch = Math::Cos(pitch / 2.0);
	double sinPitch = Math::Sin(pitch / 2.0);
	double cosYaw = Math::Cos(yaw / 2.0);
	double sinYaw = Math::Sin(yaw / 2.0);

	q0 = cosRoll * cosPitch * cosYaw + sinRoll * sinPitch * sinYaw;
	q1 = sinRoll * cosPitch * cosYaw - cosRoll * sinPitch * sinYaw;
	q2 = cosRoll * sinPitch * cosYaw + sinRoll * cosPitch * sinYaw;
	q3 = cosRoll * cosPitch * sinYaw - sinRoll * sinPitch * cosYaw;

	return Vec4{ q1, q2, q3, q0 };
}
static Vec4 QuaternionMulti(Vec4 vl, Vec4 vr) {
		Vec4 temp{
			vl.w * vr.x + vl.x * vr.w + vl.y * vr.z - vl.z * vr.y,
			vl.w * vr.y - vl.x * vr.z + vl.y * vr.w + vl.z * vr.x,
			vl.w * vr.z + vl.x * vr.y - vl.y * vr.x + vl.z * vr.w,
			vl.w * vr.w - vl.x * vr.x - vl.y * vr.y - vl.z * vr.z
		};
		return temp;
	}
```

この構造体を用いて実際に描画する際の`Main.cpp`は以下のようになります．

```cpp
# include <Siv3D.hpp>
# include "Octachoron.hpp"

void Main()
{
	Window::Resize(1280, 720);
	Font font{ 30 };
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 0, -32 } };

	double f = 2;
	double r = 0;


	Octachoron oct{ Vec4{0, 0, 0, 0}, f };


	while (System::Update())
	{
		camera.update(20);

		oct.setRotation(EulerToVec4(r, 0, 0), ogm::EulerToVec4(0, 0, 0));

		Graphics3D::SetCameraTransform(camera);
		{
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

			oct.drawFrame(f);
		}

		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}
	}
}
```

# 描画

この`Main.cpp`を実行すると以下のような描画が得られます！
4次元空間上に存在する4次元超立方体を3次元空間に透視投影することで描画を実現しています．

![20231221-083322-830.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558342/2ef25305-4c4b-acb9-89d0-cad0bcb3a8f8.png)

このような見た目になるのは透視投影だからです．
透視投影の説明の際に，手前と奥にある正方形が見えるという話をしました．
それの次元を一つ上げたものが今回の図になります．

ついでなので，`Main.cpp`の`while'ブロックの中に`r += 1_deg;`を追加し，$x$軸で回転してみました．
すると，以下のような出力になりました．
![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558342/ee47519b-2141-a78f-f086-456e4631d5e6.gif)

$x$軸での回転というシンプルな回転ですが，こんなに見た目になるのは面白いですね！！

# 終わりに
今回は4次元超立方体の描画に挑戦してみました．
4次元の世界はこんなに面白い見た目をしているのだなぁと思いました．
今後は4次元超球の描画などにも挑戦してみたいと思いました．

是非皆さんも多次元の描画に挑戦してみてください！！