影やブルームの効果にシングルトンパターンを活用すると便利【OpenSiv3D】

# はじめに
OpenSiv3Dの2D描画サンプルには、ガウシアンブラーとレンダーテクスチャを使用した多彩なテクニックが紹介されています。これらはビジュアルを豊かにするための重要な要素です。
https://siv3d.github.io/ja-jp/samples/2d/
今回は、これらのテクニックをシンプルかつ効率的に活用するため、シングルトンパターンを用いた実装を紹介します。

# シングルトンパターンの適用
シングルトンパターンを用いることで、どの場所からでも簡単にレンダーターゲットを指定し、エフェクトを適用できます。例えば、`BloomManager`を使用する場合は以下のようになります：

```cpp
{
    const auto t = BloomManager::getInstance().getRenderTarget();

    // ここに発光させたいオブジェクトを描画
}

BloomManager::getInstance().draw();
```

このコードでは、どこからでも`BloomManager`のインスタンスにアクセスし、ブルームエフェクトのレンダーターゲットを取得し、その後エフェクトを描画します。

![ezgif.com-optimize (6).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34295/d65c2ee0-0c8e-49e6-7857-02a294aa76b1.gif)
![ezgif.com-optimize (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34295/1aa9fa2a-8b00-5a7f-a4e7-3e8922b2e7d2.gif)
![ezgif.com-optimize (4).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34295/cb1992b7-8f10-5755-d5a7-fd955264bb04.gif)
![ezgif.com-optimize (5).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34295/08a46653-ccc8-c2c6-74eb-122446f7169d.gif)


# 実装

```cpp
class ShadowManager
{
private:
	RenderTexture shadowTexture, blur4, internal4;

	ShadowManager()
		: shadowTexture(Scene::Size(), ColorF{ 1.0, 0.0 }),
		blur4(Scene::Size() / 4),
		internal4(Scene::Size() / 4)
	{}

public:
	static ShadowManager& getInstance()
	{
		static ShadowManager instance;
		return instance;
	}

	[[nodiscard]] 
    std::tuple<ScopedRenderTarget2D, ScopedRenderStates2D, Transformer2D> getTargets(const Vec2& offset = Vec2(10.0, 10.0))
	{
		ScopedRenderTarget2D target{ shadowTexture};
		ScopedRenderStates2D blend{ BlendState::MaxAlpha };
		Transformer2D transform{ Mat3x2::Translate(offset) };

		return { std::move(target), std::move(blend), std::move(transform) };
	}

	void draw(double alpha = 0.5)
	{
		Shader::Downsample(shadowTexture, blur4);
		Shader::GaussianBlur(blur4, internal4, blur4);
		blur4.resized(Scene::Size()).draw(ColorF{ 0.0, alpha });
		shadowTexture.clear(ColorF{ 1.0, 0.0 });
	}
};

class BloomManager
{
private:
	RenderTexture blur1, internal1, blur4, internal4, blur8, internal8;

	// コンストラクター
	BloomManager(const Size& sceneSize)
		: blur1(sceneSize), internal1(sceneSize),
		blur4(sceneSize / 4), internal4(sceneSize / 4),
		blur8(sceneSize / 8), internal8(sceneSize / 8)
	{
		blur1.clear(ColorF{ 0.0 });
	}

public:

	static BloomManager& getInstance()
	{
		static BloomManager instance(Scene::Size());
		return instance;
	}

	[[nodiscard]] 
    ScopedRenderTarget2D getRenderTarget()
	{
		return ScopedRenderTarget2D(blur1);
	}

	void draw(double a1 = 0.5, double a4 = 0.5, double a8 = 0.5)
	{
		// ガウシアンフィルタの適用
		Shader::GaussianBlur(blur1, internal1, blur1);
		Shader::Downsample(blur1, blur4);
		Shader::GaussianBlur(blur4, internal4, blur4);
		Shader::Downsample(blur4, blur8);
		Shader::GaussianBlur(blur8, internal8, blur8);

		// ブルームエフェクトの描画
		const ScopedRenderStates2D blend{ BlendState::Additive };
		if (a1) { blur1.resized(Scene::Size()).draw(ColorF{ a1 }); }
		if (a4) { blur4.resized(Scene::Size()).draw(ColorF{ a4 }); }
		if (a8) { blur8.resized(Scene::Size()).draw(ColorF{ a8 }); }

		blur1.clear(ColorF{ 0.0 });
	}
};
```


### `ShadowManager`クラスの紹介
`ShadowManager`クラスは、影の描画を簡単に行えるように設計されています。以下の機能を提供します：

- `getTargets()`: 影を描画するための`ScopedRenderTarget2D`、`ScopedRenderStates2D`、`Transformer2D`を返します。
- `draw()`: ぼかした影を描画します。

### `BloomManager`クラスの紹介
`BloomManager`クラスは、ブルームエフェクトの管理を行うシングルトンです。以下の機能を提供します：

- `getRenderTarget()`: ブルームエフェクト用の`ScopedRenderTarget2D`を返します。
- `draw()`: ブルームエフェクトを描画します。

# 追記

ほかにもあとから追加した機能を紹介します。

## ブラー

```cpp
class BlurManager
{
public:
	// フィルタのタイプを表すenum class
	enum class FilterType {
		Original,
		GaussianBlur1x,
		GaussianBlur2x,
		DownsampleHalfBlur,
		DownsampleQuarterBlur
	};

private:
	RenderTexture blur1, internal1, blur2, internal2, blur4, internal4;

	// コンストラクターは非公開
	BlurManager(const Size& sceneSize)
		: blur1(sceneSize), internal1(sceneSize),
		blur2(sceneSize / 2), internal2(sceneSize / 2),
		blur4(sceneSize / 4), internal4(sceneSize / 4)
	{
		blur1.clear(ColorF{ 0.0 });
	}

	void applyGaussianBlur(RenderTexture& texture, RenderTexture& internalTexture)
	{
		Shader::GaussianBlur(texture, internalTexture, texture);
	}

public:
	// シングルトンインスタンスを取得するメソッド
	static BlurManager& getInstance()
	{
		static BlurManager instance(Scene::Size());
		return instance;
	}

	// ScopedRenderTarget2Dを返すメソッド
	[[nodiscard]]
	ScopedRenderTarget2D getRenderTarget()
	{
		return ScopedRenderTarget2D(blur1);
	}

	// フィルタを適用するメソッド
	void draw(FilterType filterType)
	{
		switch (filterType) {
		case FilterType::Original:
			break;
		case FilterType::GaussianBlur1x:
			applyGaussianBlur(blur1, internal1);
			blur1.draw();
			break;
		case FilterType::GaussianBlur2x:
			applyGaussianBlur(blur1, internal1);
			applyGaussianBlur(blur1, internal1);
			blur1.draw();
			break;
		case FilterType::DownsampleHalfBlur:
			Shader::Downsample(blur1, blur2);
			applyGaussianBlur(blur2, internal2);
			blur2.scaled(2.0).draw();
			break;
		case FilterType::DownsampleQuarterBlur:
			Shader::Downsample(blur1, blur4);
			applyGaussianBlur(blur4, internal4);
			blur4.scaled(4.0).draw();
			break;
		}


		// テクスチャのクリア
		blur1.clear(ColorF{ 0.0 });
		blur2.clear(ColorF{ 0.0 });
		blur4.clear(ColorF{ 0.0 });
	}
};
```

## 音量

```cpp

class GlobalVolumeManager
{
private:

	double globalVolume = 0.5;

	std::function<void()> updateFunction = std::bind(&GlobalVolumeManager::defaultUpdateFunction, this);

	GlobalVolumeManager()
	{
		GlobalAudio::SetVolume(globalVolume);
	}

	void defaultUpdateFunction()
	{
		if (SimpleGUI::Slider(U"Global Vol", globalVolume, Vec2{ 20, 20 }, 120, 200))
		{
			// グローバルオーディオの音量を変更
			GlobalAudio::SetVolume(globalVolume);
		}
	}

public:

	static GlobalVolumeManager& getInstance()
	{
		static GlobalVolumeManager instance;
		return instance;
	}

	void update()
	{
		updateFunction();
	}

	void setFunction(const std::function<void()>& _updateFunction)
	{
		updateFunction = _updateFunction;
	}

	void setGlobalVolume(double globalVolume)
	{
		GlobalAudio::SetVolume(globalVolume);
	}
};
```

## キーコンフィグ

```cpp
class KeyConfigManager
{
private:

	HashTable<String, InputGroup> inputTable;

	KeyConfigManager()
	{

	}

public:

	static KeyConfigManager& getInstance()
	{
		static KeyConfigManager instance;
		return instance;
	}

	void setKey(const String& keyName, const InputGroup& input)
	{
		inputTable[keyName] = input;
	}

	InputGroup& getKey(const String& keyName)
	{
		return inputTable[keyName];
	}

	bool getAnyKey() const
	{
		return Keyboard::GetAllInputs().size() > 0 || Mouse::GetAllInputs().size() > 0;
	}
};
```

## ウィンドウのサイズ

```cpp
class WindowManager
{
private:

	WindowManager()
	{
	}

	void defaultUpdateFunction()
	{
		if (Window::GetState().fullscreen)
		{
			if (SimpleGUI::Button(U"Window mode", Vec2{ 400, 20 }))
			{
				// ウィンドウモードにする
				Window::SetFullscreen(false);
			}
		}
		else
		{
			if (SimpleGUI::Button(U"Fullscreen mode", Vec2{ 400, 20 }))
			{
				// フルスクリーンモードにする
				Window::SetFullscreen(true);
			}
		}
	}

	std::function<void()> m_updateFunction = std::bind(&WindowManager::defaultUpdateFunction, this);

public:

	static WindowManager& getInstance()
	{
		static WindowManager instance;
		return instance;
	}

	void init(const String& windowTitle, const Size& windowSize = { 1280,720 })
	{
		Window::SetTitle(windowTitle);
		Window::Resize(windowSize);
		Window::SetStyle(WindowStyle::Sizable);
		Scene::SetResizeMode(ResizeMode::Keep);
		Scene::SetBackground(ColorF(0.0));
	}

	void update()
	{
		m_updateFunction();
	}

	void setUpdateFunction(const std::function<void()>& _updateFunction)
	{
		m_updateFunction = _updateFunction;
	}
};
```

## スクリーンキャプチャ

```cpp
class ScreenCaptureManager
{
private:

	ScreenCaptureManager()
	{
	}

	void defaultUpdateFunction()
	{
		if (SimpleGUI::Button(U"Capture", Vec2{ Scene::Size().x - 150, 20 }))
		{
			capture();
		}
	}

	std::function<void()> m_updateFunction = std::bind(&ScreenCaptureManager::defaultUpdateFunction, this);

	Texture m_tex;

	Stopwatch m_sw;

	String GenerateUniqueName()
	{
		// 現在の日時を取得
		const DateTime now = DateTime::Now();

		// 日時から文字列を生成
		String uniqueName = Format(now.year, now.month, now.day,
								   now.hour, now.minute, now.second, now.milliseconds);

		return uniqueName;
	}

public:

	static ScreenCaptureManager& getInstance()
	{
		static ScreenCaptureManager instance;

		return instance;
	}

	void capture()
	{
		ScreenCapture::RequestCurrentFrame();
	}

	void setUpdateFunction(const std::function<void()>& updateFunction)
	{
		m_updateFunction = updateFunction;
	}

	void update()
	{
		m_updateFunction();

		if (ScreenCapture::HasNewFrame())
		{
			Image image = ScreenCapture::GetFrame();

			m_tex = Texture(image);

			image.savePNG(U"photo/" + GenerateUniqueName() + U".png");

			m_sw.restart();
		}

		if (m_sw.isRunning() && m_sw.msF() < 2000)
		{
			const double ratio = Math::Max(0.0, (1.0 - m_sw.msF() * 0.002));
			const double downRatio = Math::Max(0.0, (m_sw.msF() * 0.001 - 1.5) * 2.0);

			const Vec2 centerPos = Scene::CenterF() + Vec2::Down(2000) * downRatio;

			const auto rect = Scene::Rect().scaled(0.9 + 0.1 * ratio).setCenter(centerPos);

			rect.drawShadow(Vec2(10, 10), 10.0);
			rect(m_tex).draw();

			Scene::Rect().draw(ColorF(1.0, ratio));
		}
	}
};
```

## エフェクト

```cpp
class EffectManager
{
private:

	Effect effect;

	EffectManager()
	{

	}

public:

	static EffectManager& getInstance()
	{
		static EffectManager instance;
		return instance;
	}

	void update()
	{
		effect.update();
	}

	Effect& getEffect()
	{
		return effect;
	}

};
```

## シリアル通信

```cpp
class SerialManager
{
private:
	Serial m_serial;
	Array<SerialPortInfo> m_infos;
	size_t m_selectedIndex = -1; // 選択されていない状態を表す

	// コンストラクタはプライベート
	SerialManager()
	{
		updateInformation();
	}

public:
	// シングルトンインスタンスの取得
	static SerialManager& getInstance()
	{
		static SerialManager instance;
		return instance;
	}

	void updateInformation()
	{
		m_infos = System::EnumerateSerialPorts();
		if (!m_infos.isEmpty())
		{
			m_selectedIndex = 0; // デフォルトで最初のポートを選択
		}
	}

	// シリアルポートを開く
	bool open(size_t index)
	{
		if (index >= m_infos.size())
		{
			return false;
		}

		m_selectedIndex = index;
		return m_serial.open(m_infos[index].port, 115200);
	}

	// シリアルポートを閉じる
	void close()
	{
		if (m_serial.isOpen())
		{
			m_serial.close();
		}
	}

	// データの書き込み
	bool write(char data)
	{
		if (m_serial.isOpen())
		{
			return m_serial.write(&data, sizeof(data));
		}
		return false;
	}

	// データの読み取り
	bool read(int16& data)
	{
		if (m_serial.isOpen() && m_serial.available() >= sizeof(data))
		{
			return m_serial.read(&data, sizeof(data)) == sizeof(data);
		}
		return false;
	}

	// シリアルポート情報の取得
	const Array<SerialPortInfo>& getSerialPortInfo() const
	{
		return m_infos;
	}

	// 選択されたインデックスの取得
	size_t getSelectedIndex() const
	{
		return m_selectedIndex;
	}

	// シリアルポートが開いているか確認
	bool isOpen() const
	{
		return m_serial.isOpen();
	}

	// オプションの取得
	Array<String> getOptions() const
	{
		Array<String> options;
		for (const auto& info : m_infos)
		{
			options << U"{} ({})"_fmt(info.port, info.description);
		}
		options << U"none";
		return options;
	}
};
```