3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Siv3DAdvent Calendar 2023

Day 5

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

Last updated at Posted at 2023-12-06

はじめに

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

シングルトンパターンの適用

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

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

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

BloomManager::getInstance().draw();

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

ezgif.com-optimize (6).gif
ezgif.com-optimize (3).gif
ezgif.com-optimize (4).gif
ezgif.com-optimize (5).gif

実装

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(): 影を描画するためのScopedRenderTarget2DScopedRenderStates2DTransformer2Dを返します。
  • draw(): ぼかした影を描画します。

BloomManagerクラスの紹介

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

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

追記

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

ブラー

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 });
	}
};

音量


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);
	}
};

キーコンフィグ

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;
	}
};

ウィンドウのサイズ

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;
	}
};

スクリーンキャプチャ

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));
		}
	}
};

エフェクト

class EffectManager
{
private:

	Effect effect;

	EffectManager()
	{

	}

public:

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

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

	Effect& getEffect()
	{
		return effect;
	}

};

シリアル通信

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;
	}
};
3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?