Open6

Siv3D v0.6.14 新規追加・更新サンプル

Ryo SuzukiRyo Suzuki

v0.6.13 からのアップグレード手順(Windows)

手順に従うことで、既存の v0.6.13 プロジェクトを簡単に v0.6.14 にアップグレードできます。

1. v0.6.14 のインストール

Siv3D v0.6.14 をインストールします。

2. バックアップ

手順の間違いでプロジェクトデータを消失しないよう、バックアップをとります。

3. example および engine フォルダの更新

既存のプロジェクトの example および engine フォルダの中身を、v0.6.14 新規作成プロジェクトの example および engine フォルダの中身で置き換えます。

4. Resource.rc の更新

Resource.rc をテキストエディタで開き、追加された engine ファイルのパスを加えます。v0.6.14 新規作成プロジェクトに含まれる Resource.rcが参考になります。

5. ヘッダ・ライブラリパスの更新

Visual Studio のメニューからプロジェクトのプロパティを開き、「構成」を「すべての構成」にしたうえで「構成プロパティ」→「VC++ ディレクトリ」の 3 箇所の環境変数を SIV3D_0_6_13 から SIV3D_0_6_14 に書き換えます

6. プロジェクトのリビルド

プロジェクトをリビルドします。

Ryo SuzukiRyo Suzuki

Shader::QuadWarp

指定した QuadTexture または TextureRegion を射影して描画する機能です。

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 40, Typeface::Bold };
	font.setBufferThickness(4);
	const Texture texture{ U"example/windmill.png", TextureDesc::Mipped };
	const VideoTexture videoTexture{ U"example/video/river.mp4", Loop::Yes };
	const MSRenderTexture renderTexture{ Size{ 480, 360 }, HasDepth::No, HasMipMap::Yes };

	Quad q1{ Vec2{ 100, 300 }, Vec2{ 400, 300 }, Vec2{ 480, 500 }, Vec2{ 20, 500 } };
	Quad q2{ Vec2{ 400, 200 }, Vec2{ 720, 180 }, Vec2{ 760, 400 }, Vec2{ 360, 380 } };
	Quad q3{ Vec2{ 200, 20 }, Vec2{ 600, 20 }, Vec2{ 600, 320 }, Vec2{ 200, 320 } };

	while (System::Update())
	{
		q3.p3 = Cursor::Pos();
		videoTexture.advance();

		{
			const ScopedRenderTarget2D target{ renderTexture.clear(ColorF{ 1.0 }) };

			for (int32 y = 0; y < 9; ++y)
			{
				for (int32 x = 0; x < 12; ++x)
				{
					if (IsEven(x + y))
					{
						Rect{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.6, 0.7, 0.8 });
					}
				}
			}

			font(U"QuadWarp").draw(TextStyle::OutlineShadow(0.2, ColorF{ 0.11 },
				Vec2{ 2, 2 }, ColorF{ 0.11 }), 80, Vec2{ 20, 80 });
		}

		{
			Graphics2D::Flush();
			renderTexture.resolve();

			// ミップマップを生成する
			renderTexture.generateMips();
		}

		{
			// テクスチャを QuadWarp で大きく傾けて描画する場合、ミップマップ使用 + Aniso が効果的
			const ScopedRenderStates2D sampler{ SamplerState::ClampAniso };

			Shader::QuadWarp(q1, texture);
			Shader::QuadWarp(q2, videoTexture.getTexture());
			Shader::QuadWarp(q3, renderTexture);
		}
	}
}
Ryo SuzukiRyo Suzuki

OpenAI の新しい API

Vision

# include <Siv3D.hpp>

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	// 環境変数から API キーを取得する(事前に設定しておく)
	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");

	const Texture texture{ U"example/windmill.png" };

	OpenAI::Vision::Request request;
	request.images << OpenAI::Vision::ImageData::Base64FromFile(U"example/windmill.png");
	request.questions = U"何が写っていますか?";

	// 非同期タスク
	AsyncHTTPTask task = OpenAI::Vision::CompleteAsync(API_KEY, request);

	// 回答を格納する変数
	String answer;

	while (System::Update())
	{
		// ChatGPT の応答を待つ間はローディング画面を表示する
		if (task.isDownloading())
		{
			Circle{ Scene::Center(), 50 }.drawArc((Scene::Time() * 120_deg), 300_deg, 4, 4);
		}

		// 非同期処理が完了し、正常なレスポンスである場合
		if (task.isReady() && task.getResponse().isOK())
		{
			// 非同期処理の結果を取得する
			answer = OpenAI::Vision::GetContent(task.getAsJSON());
		}

		texture.fitted(Size{ 400, 300 }).draw(40, 40);

		// 回答がある場合
		if (answer)
		{
			font(answer).draw(20, Rect{ 40, 340, 1200, 240 }, ColorF{ 0.25 });
		}
	}
}

Vision (2)

https://twitter.com/Reputeless/status/1741416269579268285

# include <Siv3D.hpp>

Optional<int32> GetRectIndex(const Point& pos, const std::array<Rect, 2>& imageAreas)
{
	for (size_t i = 0; i < imageAreas.size(); ++i)
	{
		if (imageAreas[i].intersects(pos))
		{
			return static_cast<int32>(i);
		}
	}

	return none;
}

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };
	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");

	std::array<Image, 2> images;
	std::array<Texture, 2> textures;
	std::array<DynamicTexture, 2> dtextures;
	std::array<Rect, 2> imageAreas;

	for (int32 i = 0; i < 2; ++i)
	{
		imageAreas[i] = Rect{ (40 + i * 600), 40, 580, 320 };
	}

	// テキストボックスの中身
	TextEditState textEditState;

	// 非同期タスク
	AsyncHTTPTask task;

	// 回答を格納する変数
	String answer;

	while (System::Update())
	{
		for (int32 i = 0; i < 2; ++i)
		{
			const auto& imageArea = imageAreas[i];
			imageArea.drawFrame(16, 4, ColorF{ 0.0, 0.2 });

			if (textures[i])
			{
				textures[i].fitted(imageArea.size).drawAt(imageArea.center());
			}
			else if (dtextures[i])
			{
				dtextures[i].drawAt(imageArea.center());
			}

			if (imageArea.rightClicked())
			{
				if (dtextures[i])
				{
					images[i].fill(Palette::White);
					dtextures[i].fill(images[i]);
				}
				else if (images[i])
				{
					images[i].release();
					textures[i].release();
				}
				else
				{
					images[i] = Image{ imageArea.size, Palette::White };
					dtextures[i] = DynamicTexture{ images[i] };
				}
			}

			if (dtextures[i])
			{
				if (MouseL.pressed())
				{
					// 書き込む線の始点は直前のフレームのマウスカーソル座標
					// (初回はタッチ操作時の座標のジャンプを防ぐため、現在のマウスカーソル座標にする)
					const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());

					// 書き込む線の終点は現在のマウスカーソル座標
					const Point to = Cursor::Pos();

					// image に線を書き込む
					Line{ from, to }.movedBy(-imageArea.pos).overwrite(images[i], 4, Color{ 0 });

					// 書き込み終わった image でテクスチャを更新
					dtextures[i].fill(images[i]);
				}
			}
		}

		if (DragDrop::HasNewFilePaths())
		{
			if (const Array<DroppedFilePath> items = DragDrop::GetDroppedFilePaths())
			{
				const auto& item = items.front();

				if (const auto index = GetRectIndex(item.pos, imageAreas))
				{
					images[*index] = Image{ item.path }.fit(Size{ 512, 512 }, AllowScaleUp::No);
					textures[*index] = Texture{ images[*index], TextureDesc::Mipped };
				}

				answer.clear();
			}
		}

		// テキストボックスを表示する
		if (SimpleGUI::TextBox(textEditState, Vec2{ 40, 380 }, 600))
		{
			answer.clear();
		}

		if (SimpleGUI::Button(U"送信", Vec2{ 660, 380 }, 80,
			((not textEditState.text.isEmpty()) // テキストボックスが空でなく
				&& (not task.isDownloading())))) // タスクの実行中でないときだけボタンを有効にする
		{
			// 前回の回答を消去する
			answer.clear();

			OpenAI::Vision::Request request;
			request.questions = textEditState.text;

			for (const auto& image : images)
			{
				if (image)
				{
					request.images << OpenAI::Vision::ImageData::Base64FromImage(image);
				}
			}

			// タスクを作成する
			task = OpenAI::Vision::CompleteAsync(API_KEY, request);
		}

		// ChatGPT の応答を待つ間はローディング画面を表示する
		if (task.isDownloading())
		{
			Circle{ Scene::Center().movedBy(0, 180), 50 }.drawArc((Scene::Time() * 120_deg), 300_deg, 4, 4);
		}

		// 非同期処理が完了し、正常なレスポンスである場合
		if (task.isReady() && task.getResponse().isOK())
		{
			// 非同期処理の結果を取得する
			answer = OpenAI::Vision::GetContent(task.getAsJSON());
		}

		// 回答がある場合
		if (answer)
		{
			font(answer).draw(20, Rect{ 40, 440, 1200, 600 }, ColorF{ 0.25 });
		}
	}
}

Image (DALLE-3)

# include <Siv3D.hpp>

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// API キーを環境変数から取得する
	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");

	// プロンプトとテクスチャ
	TextEditState textEditState;
	TextAreaEditState textAreaEditState;
	Texture texture;

	// 非同期タスク
	AsyncHTTPTask task1;
	AsyncTask<Image> task2;

	while (System::Update())
	{
		SimpleGUI::TextBox(textEditState, Vec2{ 40, 40 }, 240);

		if (SimpleGUI::Button(U"に登場する敵モンスターを生成", Vec2{ 300, 40 }, 360,
			((not textEditState.text.isEmpty()) // テキストボックスが空でなく
				&& (not task1.isDownloading())))) // タスクの実行中でないときだけボタンを有効にする
		{
			// 前回の内容を消去する
			textAreaEditState.clear();
			texture.release();

			const String prompt = U"ファンタジー世界で{0}に登場する敵モンスターを 1 種類考えてイメージを膨らませ、{0}とそこにいるそのモンスターのイラストを描くプロンプトのみを英語で出力せよ。イラストに文字を含むと罰金が科される。"_fmt(textEditState.text); // 「罰金」は強い禁止のためのおまじない
			task1 = OpenAI::Chat::CompleteAsync(API_KEY, prompt, OpenAI::Chat::Model::GPT4_Turbo_Preview);
		}

		SimpleGUI::TextArea(textAreaEditState, Vec2{ 40, 80 }, Size{ 1200, 140 }, 4000);

		if (task1.isDownloading() || task2.isValid())
		{
			// ロード中の円アニメーション
			Circle{ Scene::Center(), 50 }.drawArc((Scene::Time() * 120_deg), 300_deg, 4, 4);
		}

		if (task1.isReady() && task1.getResponse().isOK())
		{
			textAreaEditState = TextAreaEditState{ OpenAI::Chat::GetContent(task1.getAsJSON()) };
		}

		if (task2.isReady())
		{
			texture = Texture{ task2.get(), TextureDesc::Mipped };
		}

		if (SimpleGUI::Button(U"画像を生成", Vec2{ 40, 240 }, 180, ((not textAreaEditState.text.isEmpty()) // テキストボックスが空でなく
			&& (not task2.isValid())))) // タスクの実行中でないときだけボタンを有効にする
		{
			task2 = OpenAI::Image::CreateAsync(API_KEY, OpenAI::Image::RequestDALLE3{
				.prompt = textAreaEditState.text,
				.imageSize = OpenAI::Image::RequestDALLE3::ImageSize1792x1024 });
		}

		if (texture)
		{
			texture.scaled(0.45).draw(240, 240);
		}
	}
}

Speech

https://twitter.com/Reputeless/status/1726260529847754849

# include <Siv3D.hpp>

class Task
{
public:

	enum class State
	{
		Running,
		Failed,
		Completed,
	};

	Task() = default;

	void update()
	{
		if (m_isReady)
		{
			return;
		}

		if (m_task.isReady())
		{
			m_isReady = true;
			m_isFailed = (not m_task.get());
			m_completeTime = Time::GetMillisec();
		}
	}

	void play()
	{
		if (not m_isReady)
		{
			return;
		}

		if (not m_audio)
		{
			m_audio = Audio{ Audio::Stream, m_path };
		}

		m_audio.play();
	}

	const String& getText() const
	{
		return m_request.input;
	}

	const Audio& getAudio() const
	{
		return m_audio;
	}

	State getState() const
	{
		if (not m_isReady)
		{
			return State::Running;
		}
		else if (m_isFailed)
		{
			return State::Failed;
		}
		else
		{
			return State::Completed;
		}
	}

	static Task Create(const StringView apiKey, const OpenAI::Speech::Request& request, const FilePathView saveDirectory)
	{
		Task task;
		task.m_request = request;
		task.m_path = FileSystem::PathAppend(saveDirectory, U"{}.{}"_fmt(UUIDValue::Generate().str(), request.responseFormat));
		task.m_task = OpenAI::Speech::CreateAsync(apiKey, request, task.m_path);
		task.m_startTime = Time::GetMillisec();
		return task;
	}

	double getTime() const
	{
		if (not m_isReady)
		{
			return 0.0;
		}
		else
		{
			return (m_completeTime - m_startTime) / 1000.0;
		}
	}

private:

	OpenAI::Speech::Request m_request;

	FilePath m_path;

	AsyncTask<bool> m_task;

	uint64 m_startTime = 0;

	uint64 m_completeTime = 0;

	Audio m_audio;

	bool m_isReady = false;

	bool m_isFailed = false;
};

void DrawTaskShadow(int32 taskIndex)
{
	const Rect rect{ 40, (40 + taskIndex * 60), 1200, 56 };
	rect.drawShadow({ 0, 2 }, 6, 0.0, ColorF{ 0.0, 0.5 }, false);
}

void DrawTask(int32 taskIndex, Task& task, const Font& font)
{
	const double fontSize = 20.0;
	const Rect rect{ 40, (40 + taskIndex * 60), 1200, 56 };

	rect.draw();
	{
		const auto text = font(task.getText());
		const double textWidth = text.region(fontSize).w;
		const double overWidth = (textWidth - 846.0 + 100);
		const Audio& audio = task.getAudio();
		const Rect textRect = Rect{ rect.pos.movedBy(54, 14), 846, 30 };
		const ScopedViewport2D viewport{ textRect };

		if (0 < overWidth)
		{
			const double audioProgress = (audio.posSec() / audio.lengthSec());
			const double xOffset = (overWidth * audioProgress);
			text.draw(fontSize, Vec2{ 10 - xOffset, 0 }, ColorF{ 0.11 });
		}
		else
		{
			text.draw(fontSize, Vec2{ 10, 0 }, ColorF{ 0.11 });
		}
	}

	Rect{ rect.pos.movedBy(900, 00), 300, 56 }.draw(Arg::left(0.8, 0.9, 1.0), Arg::right(0.9, 0.95, 1.0));

	if (task.getState() == Task::State::Completed)
	{
		Rect{ rect.pos, 50, rect.h }.draw(ColorF{ 0.2, 0.7, 0.5 });
		font(U"#{}"_fmt(taskIndex)).draw(fontSize, Arg::leftCenter(rect.pos.movedBy(12, 28)), ColorF{ 1.0 });

		if (SimpleGUI::Button(U"\U000F040A", rect.pos.movedBy(920, 10)))
		{
			task.play();
		}

		font(U"生成時間 {:.2f} 秒"_fmt(task.getTime())).draw(fontSize, Arg::rightCenter(rect.pos.movedBy(1180, 28)), ColorF{ 0.1, 0.2, 0.5 });
	}
	else
	{
		font(U"#{}"_fmt(taskIndex)).draw(fontSize, Arg::leftCenter(rect.pos.movedBy(12, 28)), ColorF{ 0.11 });
	}
}

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 48, Typeface::Medium };

	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");
	Array<Task> tasks;

	TextAreaEditState textAreaEditState;
	const Array<String> voices = {
		U"Alloy", U"Echo", U"Fable", U"Onyx", U"Nova", U"Shimmer",
	};
	size_t voiceIndex = 0;
	size_t qualityIndex = 0;

	size_t randomTextIndex = 0;

	const Array<String> randomTexts = {
		U"The transcriptions API takes as input the audio file you want to transcribe and the desired output file format for the transcription of the audio. We currently support multiple input and output file formats.",
		U"Please note that our Usage Policies require you to provide a clear disclosure to end users that the TTS voice they are hearing is AI-generated and not a human voice.",
		U"Siv3D には 2D, 3D ゲーム、メディアアート、ビジュアライザ、シミュレータを効率的に開発するための、便利なクラスや関数が用意されています。",
		U"Siv3D の API とサンプルは、最新の C++ 規格「C++20」で書かれています。Siv3D を使っているだけで、現代的な C++ の書き方が身に付きます。",
	};

	while (System::Update())
	{
		for (auto& task : tasks)
		{
			task.update();
		}

		for (int32 i = 0; auto & task : tasks)
		{
			DrawTaskShadow(i++);
		}

		for (int32 i = 0; auto & task : tasks)
		{
			DrawTask(i++, task, font);
		}

		SimpleGUI::TextArea(textAreaEditState, Vec2{ 40, 560 }, SizeF{ 1000, 100 });

		if (SimpleGUI::Button(U"Random", Vec2{ 1060, 600 }, 140))
		{
			textAreaEditState = TextAreaEditState{ randomTexts[randomTextIndex] };
			++randomTextIndex %= randomTexts.size();
		}

		SimpleGUI::HorizontalRadioButtons(voiceIndex, voices, Vec2{ 40, 520 });

		SimpleGUI::HorizontalRadioButtons(qualityIndex, { U"速度", U"品質" }, Vec2{ 840, 520 });

		if (SimpleGUI::Button(U"Generate", Vec2{ 1060, 520 }, 140, (not textAreaEditState.text.isEmpty())))
		{
			OpenAI::Speech::Request request;
			request.model = (qualityIndex == 0) ? OpenAI::Speech::Model::TTS1 : OpenAI::Speech::Model::TTS1HD;
			request.input = textAreaEditState.text;
			request.voice = voices[voiceIndex].lowercased();
			tasks << Task::Create(API_KEY, request, U"speech/");

			textAreaEditState.clear();
		}
	}
}
Ryo SuzukiRyo Suzuki

Shape2D::Astroid()

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.3, 0.4, 0.5 });

	while (System::Update())
	{
		Shape2D::Astroid(Vec2{ 200, 300 }, 150, 200).draw();

		for (int32 i = 0; i < 6; ++i)
		{
			Shape2D::Astroid(Vec2{ 500, (100 + i * 80) }, 30, 40, (i * 30_deg)).draw();
		}
	}
}
Ryo SuzukiRyo Suzuki

Shader::GaussianBlur() のフィルタオプション

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Texture texture{ U"example/windmill.png" };
	RenderTexture internalTexture{ texture.size() }, blurredTexture{ texture.size() };

	size_t option = 2;
	Shader::GaussianBlur(texture, internalTexture, blurredTexture, BoxFilterSize::BoxFilter9x9);

	while (System::Update())
	{
		blurredTexture.draw(40, 40);

		if (SimpleGUI::RadioButtons(option, { U"none", U"5x5", U"9x9", U"13x13"}, Vec2{ 600, 40 }))
		{
			if (option == 0)
			{
				Shader::Copy(texture, blurredTexture);
			}
			if (option == 1)
			{
				Shader::GaussianBlur(texture, internalTexture, blurredTexture, BoxFilterSize::BoxFilter5x5);
			}
			else if (option == 2)
			{
				Shader::GaussianBlur(texture, internalTexture, blurredTexture, BoxFilterSize::BoxFilter9x9);
			}
			else if (option == 3)
			{
				Shader::GaussianBlur(texture, internalTexture, blurredTexture, BoxFilterSize::BoxFilter13x13);
			}
		}
	}
}
Ryo SuzukiRyo Suzuki

3D 描画におけるシャドウ(サンプル)

# include <Siv3D.hpp> // Siv3D v0.6.14

/// @brief シャドウマップ
class ShadowMap
{
public:

	/// @brief シャドウマップの設定
	struct Config
	{
		/// @brief シャドウマップの解像度
		Size resolution = Size{ 1024, 1024 };

		/// @brief 光の漏れを軽減する係数 [0.0, 1.0]
		/// @remark 大きいと光の漏れが軽減されますが、影のソフトさが低下します。
		double lightBleedingReduction = 0.8;

		/// @brief シャドウマップ生成に MSAA を使用するか
		bool useMSAA = false;

		/// @brief フィルタオプション [0, 3]
		/// @remark 0: フィルタなし, 1: 5x5, 2: 9x9, 3: 13x13
		size_t filter = 0;

		/// @brief シャドウマップにミップマップを使用するか
		bool useMipMap = false;
	};

	/// @brief シャドウマップ用の平行光源
	struct Light
	{
		/// @brief メイン平行光源の注視点
		Vec3 focusPos{ 0, 0, 0 };

		/// @brief メイン平行光源の位置
		Vec3 sunPos = (focusPos + Graphics3D::DefaultSunDirection * 64.0);

		/// @brief メイン平行光源の上方向
		Vec3 upDirection{ 0, 1, 0 };

		/// @brief シャドウマップのビューのサイズ
		/// @remark 大きいと広い範囲をカバーできますが、詳細が失われます。
		double viewSize = 64.0;

		/// @brief シャドウマップの近面までの距離
		double nearZ = 1.0;

		/// @brief シャドウマップの遠面までの距離
		double farZ = 1024.0;
	};

	ShadowMap() = default;

	explicit ShadowMap(const Config& config)
		: m_config{ config }
		, m_initialized{ true }
	{
		updateTextures();
	}

	/// @brief デプスパスを開始します。
	/// @param light 平行光源の設定
	void beginDepth(const Light& light)
	{
		if (not m_initialized)
		{
			throw Error{ U"ShadowMap::beginDepth() called before initialization" };
		}

		if (m_depthPass)
		{
			throw Error{ U"ShadowMap::beginDepth() called twice" };
		}

		if (m_shadowPass)
		{
			throw Error{ U"ShadowMap::beginDepth() called while shadow pass is active" };
		}

		m_depthPass.emplace();
		m_depthPass->oldCameraMat = FromMat4x4(Graphics3D::GetCameraTransform());
		m_depthPass->oldEyePosition = Graphics3D::GetEyePosition();
		m_depthPass->renderTarget = std::make_unique<ScopedRenderTarget3D>(m_textures[0]->clear(ColorF{ 0.0 }));
		m_depthPass->renderStates = std::make_unique<ScopedRenderStates3D>(BlendState::Opaque);

		m_camera.update(light);
		Graphics3D::SetCameraTransform(m_camera.viewProj, light.sunPos);

		Mat4x4 worldToShadowMap = (m_camera.viewProj * Mat4x4::Scale(Float3{ 0.5, 0.5, 1.0 }) * Mat4x4::Translate(Float3{ 0.5, 0.5, 0.0 }));

		if (System::GetRendererType() != EngineOption::Renderer::Direct3D11)
		{
			worldToShadowMap = worldToShadowMap.transposed();
		}

		m_cbp->worldToShadowMap = worldToShadowMap;
		m_cbp->sunPosition = light.sunPos;
		m_cbp->lightBleedingReduction = static_cast<float>(m_config.lightBleedingReduction);
		Graphics3D::SetPSConstantBuffer(4, m_cbp);
	}

	/// @brief デプスパスを終了します。
	void endDepth()
	{
		if (not m_depthPass)
		{
			throw Error{ U"ShadowMap::endDepth() called without beginDepth()" };
		}

		if (m_shadowPass)
		{
			throw Error{ U"ShadowMap::endDepth() called while shadow pass is active" };
		}

		Graphics3D::SetCameraTransform(Mat4x4{ m_depthPass->oldCameraMat.data() }, m_depthPass->oldEyePosition);

		m_depthPass.reset();
		Graphics3D::Flush();

		if (m_config.useMSAA)
		{
			dynamic_cast<MSRenderTexture*>(m_textures[0].get())->resolve();
		}

		if (m_config.filter)
		{
			const BoxFilterSize filterSize = ToBoxFilterSize(m_config.filter);

			if (m_config.useMSAA)
			{
				Shader::GaussianBlur(*m_textures[0], *m_textures[1], *m_textures[2], filterSize);
			}
			else
			{
				Shader::GaussianBlur(*m_textures[0], *m_textures[1], *m_textures[0], filterSize);
			}

			Graphics2D::Flush();
		}

		if (m_config.useMipMap)
		{
			getDepthTexture().generateMips();
		}
	}

	/// @brief シャドウパスを開始します。
	void beginShadow()
	{
		if (not m_initialized)
		{
			throw Error{ U"ShadowMap::beginShadow() called before initialization" };
		}

		if (m_shadowPass)
		{
			throw Error{ U"ShadowMap::beginShadow() called twice" };
		}

		if (m_depthPass)
		{
			throw Error{ U"ShadowMap::beginShadow() called while depth pass is active" };
		}

		m_shadowPass.emplace();
		m_shadowPass->pRenderStates = std::make_unique<ScopedRenderStates3D>(ScopedRenderStates3D::SamplerStateInfo{ ShaderStage::Pixel, 3, (m_config.useMipMap ? SamplerState::ClampAniso : SamplerState::ClampLinear) });
		Graphics3D::SetPSTexture(1, getDepthTexture());
		Graphics3D::SetPSConstantBuffer(4, m_cbp);
	}

	/// @brief シャドウパスを終了します。
	void endShadow()
	{
		if (not m_shadowPass)
		{
			throw Error{ U"ShadowMap::endShadow() called without beginShadow()" };
		}

		if (m_depthPass)
		{
			throw Error{ U"ShadowMap::endShadow() called while depth pass is active" };
		}

		Graphics3D::SetPSTexture(1, none);
		m_shadowPass.reset();
	}

	/// @brief シャドウマップの設定を変更します。
	/// @param config シャドウマップの設定
	void setConfig(const Config& config)
	{
		if (m_depthPass || m_shadowPass)
		{
			throw Error{ U"ShadowMap::setConfig() called while depth or shadow pass is active" };
		}

		m_config = config;
		updateTextures();
	}

	/// @brief 深度テクスチャを取得します。
	/// @return 深度テクスチャ
	[[nodiscard]]
	const RenderTexture& getDepthTexture() const noexcept
	{
		return *m_textures[getDepthTextureIndex()];
	}

private:

	struct TextureState
	{
		Size resolution{ 0, 0 };
		bool isRequired = false;
		bool useMSAA = false;
		bool useDepth = false;
		bool useMipMap = false;

		[[nodiscard]]
		bool operator ==(const TextureState&) const = default;
	};

	struct DepthCamera
	{
		Mat4x4 viewProj = Mat4x4::Identity();

		void update(const Light& light)
		{
			const SIMD_Float4 eyePosition{ light.sunPos, 0.0f };
			const SIMD_Float4 focusPosition{ light.focusPos, 0.0f };
			const SIMD_Float4 upDirection{ light.upDirection, 0.0f };
			const Mat4x4 view = DirectX::XMMatrixLookAtLH(eyePosition, focusPosition, upDirection);
			const float viewF = static_cast<float>(light.viewSize);
			const Mat4x4 proj = DirectX::XMMatrixOrthographicLH(viewF, viewF, static_cast<float>(light.farZ), static_cast<float>(light.nearZ));
			viewProj = (view * proj);
		}
	};

	struct DepthPass
	{
		std::array<float, 16> oldCameraMat = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f };
		Float3 oldEyePosition{ 0, 0, 0 };
		std::unique_ptr<ScopedRenderTarget3D> renderTarget;
		std::unique_ptr<ScopedRenderStates3D> renderStates;
	};

	struct ShadowPass
	{
		std::unique_ptr<ScopedRenderStates3D> pRenderStates;
	};

	struct PSShadow
	{
		Mat4x4 worldToShadowMap;
		Float3 sunPosition{ 0,0,0 };
		float lightBleedingReduction = 0.8f;
	};

	static constexpr TextureFormat DepthFormat = TextureFormat::R32G32_Float;

	Config m_config;

	std::array<TextureState, 3> m_currentTextureStates;

	std::array<std::unique_ptr<RenderTexture>, 3> m_textures;

	DepthCamera m_camera;

	Optional<DepthPass> m_depthPass;

	Optional<ShadowPass> m_shadowPass;

	ConstantBuffer<PSShadow> m_cbp;

	bool m_initialized = false;

	static void CreateDepthTexture(std::unique_ptr<RenderTexture>& depthTexture, const Config& config)
	{
		if (depthTexture)
		{
			const bool currentMSAA = (dynamic_cast<const MSRenderTexture*>(depthTexture.get()) != nullptr);
			const bool currentMipMap = depthTexture->hasMipMap();
			const Size currentResolution = depthTexture->size();

			if ((config.useMSAA == currentMSAA)
				&& (config.useMipMap == currentMipMap)
				&& (config.resolution == currentResolution))
			{
				return;
			}
		}

		depthTexture.reset();

		if (config.useMSAA)
		{
			depthTexture = std::make_unique<MSRenderTexture>(config.resolution, DepthFormat, HasDepth::Yes, HasMipMap{ config.useMipMap });
		}
		else
		{
			depthTexture = std::make_unique<RenderTexture>(config.resolution, DepthFormat, HasDepth::Yes, HasMipMap{ config.useMipMap });
		}
	}

	static void CreateInternalTexture(std::unique_ptr<RenderTexture>& internalTexture1, std::unique_ptr<RenderTexture>& internalTexture2, const Config& config)
	{
		{
			if (internalTexture1)
			{
				const Size currentResolution = internalTexture1->size();

				if (config.resolution == currentResolution)
				{
					return;
				}
			}

			internalTexture1.reset();
			internalTexture1 = std::make_unique<RenderTexture>(config.resolution, DepthFormat);
		}

		{
			if (not config.useMSAA)
			{
				if (internalTexture2)
				{
					internalTexture2.reset();
					return;
				}
			}

			if (internalTexture2)
			{
				const Size currentResolution = internalTexture2->size();

				if (config.resolution == currentResolution)
				{
					return;
				}
			}

			internalTexture2.reset();
			internalTexture2 = std::make_unique<RenderTexture>(config.resolution, DepthFormat);
		}
	}

	[[nodiscard]]
	size_t getDepthTextureIndex() const noexcept
	{
		if (m_config.filter)
		{
			return (m_config.useMSAA ? 2 : 1);
		}
		else
		{
			return 0;
		}
	}

	[[nodiscard]]
	static std::unique_ptr<RenderTexture> CreateRenderTexture(const TextureState& textureState)
	{
		if (not textureState.isRequired)
		{
			return nullptr;
		}

		if (textureState.useMSAA)
		{
			return std::make_unique<MSRenderTexture>(
				textureState.resolution,
				DepthFormat,
				HasDepth{ textureState.useDepth },
				HasMipMap{ textureState.useMipMap });
		}
		else
		{
			return std::make_unique<RenderTexture>(
				textureState.resolution,
				DepthFormat,
				HasDepth{ textureState.useDepth },
				HasMipMap{ textureState.useMipMap });
		}
	}

	void updateTextures()
	{
		if (not m_initialized)
		{
			return;
		}

		const size_t depthTextureIndex = getDepthTextureIndex();

		std::array<TextureState, 3> requiredTextureStates;

		// [0]
		{
			requiredTextureStates[0] =
			{
				.resolution = m_config.resolution,
				.isRequired = true,
				.useMSAA = m_config.useMSAA,
				.useDepth = true,
				.useMipMap = (m_config.useMipMap && (depthTextureIndex == 0)),
			};
		}

		// [1]
		{
			requiredTextureStates[1] =
			{
				.resolution = m_config.resolution,
				.isRequired = static_cast<bool>(m_config.filter),
				.useMSAA = false,
				.useDepth = false,
				.useMipMap = (m_config.useMipMap && (depthTextureIndex == 1)),
			};
		}

		// [2]
		{
			requiredTextureStates[2] =
			{
				.resolution = m_config.resolution,
				.isRequired = (m_config.filter && m_config.useMSAA),
				.useMSAA = false,
				.useDepth = false,
				.useMipMap = (m_config.useMipMap && (depthTextureIndex == 2)),
			};
		}

		for (size_t i = 0; i < 3; ++i)
		{
			if (m_currentTextureStates[i] == requiredTextureStates[i])
			{
				continue;
			}

			m_textures[i] = CreateRenderTexture(requiredTextureStates[i]);
			m_currentTextureStates[i] = requiredTextureStates[i];
		}
	}

	static std::array<float, 16> FromMat4x4(const Mat4x4& mat)
	{
		std::array<float, 16> result{};
		std::memcpy(result.data(), &mat, (sizeof(float) * 16));
		return result;
	}

	static BoxFilterSize ToBoxFilterSize(const size_t filter)
	{
		if (3 <= filter)
		{
			return BoxFilterSize::BoxFilter13x13;
		}
		else if (filter == 2)
		{
			return BoxFilterSize::BoxFilter9x9;
		}
		else
		{
			return BoxFilterSize::BoxFilter5x5;
		}
	}
};

void DrawObjects(double time)
{
	Box::FromPoints({ -8, 0, 5 }, { 0, 0.5, 7 }).draw();
	Box::FromPoints({ -0.5, 0, 5 }, { 0, 1.8, 7 }).draw();
	Box::FromPoints({ -0.5, 1.8, 5 }, { 6, 2.0, 7 }).draw();

	for (int32 x = -2; x <= 2; ++x)
	{
		Cylinder{ Vec3{ (x * 4), 0, 4 }, Vec3{ (x * 4), 4, 4 }, 0.3 }.draw(HSV{ x * 80, 0.3, 1.0 }.removeSRGBCurve());
	}

	for (int32 y = 2; y < 8; ++y)
	{
		Cylinder{ Vec3{ 0, y, -4 }, Vec3{ 4, y, -4 }, 0.1 }.draw(HSV{ 80, 0.3, 1.0 }.removeSRGBCurve());
	}

	Cylinder{ Vec3{ 0, 0, -4 }, Vec3{ 0, 8, -4 }, 0.1 }.draw(HSV{ 80, 0.3, 1.0 }.removeSRGBCurve());
	Cylinder{ Vec3{ 0, 0, -4 }, Vec3{ 0, 0.2, -4 }, 0.5 }.draw(HSV{ 80, 0.3, 1.0 }.removeSRGBCurve());
	Cylinder{ Vec3{ 4, 0, -4 }, Vec3{ 4, 8, -4 }, 0.1 }.draw(HSV{ 80, 0.3, 1.0 }.removeSRGBCurve());
	Cylinder{ Vec3{ 4, 0, -4 }, Vec3{ 4, 0.2, -4 }, 0.5 }.draw(HSV{ 80, 0.3, 1.0 }.removeSRGBCurve());
	Cylinder{ Vec3{ 8, 1.0, -5 }, Vec3{ 8, 9.0, -5 }, 0.1 }.draw(HSV{ 160, 0.3, 1.0 }.removeSRGBCurve());
	Cylinder{ Vec3{ 8, 1.0, -5 }, Vec3{ 8, 1.2, -5 }, 0.5 }.draw(HSV{ 160, 0.3, 1.0 }.removeSRGBCurve());
	Box::FromPoints({ -8, 0, -2 }, { -7, 8, 2 }).draw();
	Box::FromPoints({ -6, 0, -8 }, { -5.5, 6, -4 }).oriented(Quaternion::RotateY(-45_deg)).draw(Linear::Palette::Limegreen);
	Box::FromPoints({ 6, 0, -6 }, { 9, 1, -3 }).oriented(Quaternion::RotateY(-45_deg)).draw(Linear::Palette::Skyblue);

	const double sphereY = (2 + Periodic::Jump0_1(1.6s, time) * 4);
	Sphere{ Vec3{ 1, sphereY, 0 }, 2.0 }.draw();

	OrientedBox{ Arg::bottomCenter(6, 0, 0), { 4, 1.5, 0.2 },  Quaternion::RotateY(time * 50_deg) }.draw();
	Cylinder{ Vec3{ 6, 0, 0 }, Vec3{ 6, 2, 0 }, 0.2 }.draw(Linear::Palette::Purple);

	OrientedBox{ Vec3{ -2, 6, -6 }, 1, 8, 1, Quaternion::RollPitchYaw(0, 45_deg, time * 60_deg) }.draw(Linear::Palette::Skyblue);
	OrientedBox{ Vec3{ -2, 6, -6 }, 8, 1, 1, Quaternion::RollPitchYaw(0, 45_deg, time * 60_deg) }.draw(Linear::Palette::Skyblue);
}

void Main()
{
	Window::Resize(1280, 720);
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const Texture uvChecker{ U"example/texture/uv.png", TextureDesc::MippedSRGB };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	// シャドウマップの設定
	ShadowMap::Config config
	{
		.resolution = Size{ 1024, 1024 },
		.lightBleedingReduction = 0.8,
		.useMSAA = false,
		.filter = 0,
		.useMipMap = false,
	};

	// シャドウマップ用の平行光源設定
	ShadowMap::Light light
	{
		.focusPos = Vec3{ 0, 0, 0 },
		.viewSize = 22.0,
	};

	// シャドウマップ
	ShadowMap shadowMap{ config };

	// シャドウマップ用のシェーダ
	const PixelShader psDepth = HLSL{ U"example/shader/hlsl/default3d_forward_shadow.hlsl", U"Depth_PS" }
		| GLSL{ U"example/shader/glsl/default3d_forward_shadow_depth.frag", {{ U"PSPerFrame", 0 }, { U"PSPerView", 1 }, { U"PSPerMaterial", 3 }, { U"PSShadow", 4 }} };
	const PixelShader psShadow = HLSL{ U"example/shader/hlsl/default3d_forward_shadow.hlsl", U"Shading_PS" }
		| GLSL{ U"example/shader/glsl/default3d_forward_shadow_shading.frag", {{ U"PSPerFrame", 0 }, { U"PSPerView", 1 }, { U"PSPerMaterial", 3 }, { U"PSShadow", 4 }} };
	if ((not psDepth) || (not psShadow))
	{
		throw Error{ U"Failed to load shader" };
	}

	size_t resolutionOption = 4;
	double time = 8.0;
	bool paused = false;
	bool showDepth = true;
	bool showUI = true;

	while (System::Update())
	{
		if (not paused)
		{
			time += Scene::DeltaTime();
		}

		camera.update(2.0);
		Graphics3D::SetCameraTransform(camera);

		// メイン光源の位置を更新する
		double halfDay0_1 = Periodic::Sawtooth0_1(60s, time);
		const Quaternion q = (Quaternion::RotateY(halfDay0_1 * 180_deg) * Quaternion::RotateX(50_deg));
		const Vec3 sunDirection = (q * Vec3::Right());
		Graphics3D::SetSunDirection(sunDirection);
		Graphics3D::SetGlobalAmbientColor(ColorF{ 0.25 });
		light.sunPos = (light.focusPos + sunDirection * 64.0);

		// デプスパス(シャドウマップの生成)
		{
			shadowMap.beginDepth(light);
			{
				const ScopedCustomShader3D shader{ psDepth };
				Plane{ 64 }.draw(); // デプスパスではテクスチャを使用しない
				DrawObjects(time);
			}
			shadowMap.endDepth();
		}

		// シャドウパス(シャドウマップを使用した描画)
		{
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			shadowMap.beginShadow();
			{
				const ScopedCustomShader3D shader{ psShadow };
				Plane{ 64 }.draw(uvChecker);
				DrawObjects(time);
			}
			shadowMap.endShadow();
		}

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

		// UI
		if (showUI)
		{
			if (SimpleGUI::Slider(U"lightBleedingReduction: {:.2f}"_fmt(config.lightBleedingReduction), config.lightBleedingReduction, 0.0, 1.0, Vec2{ 860, 40 }, 280, 120))
			{
				shadowMap.setConfig(config);
			}

			if (SimpleGUI::RadioButtons(config.filter, { U"none", U"5x5", U"9x9", U"13x13" }, Vec2{ 936, 80 }, 160))
			{
				shadowMap.setConfig(config);
			}

			if (SimpleGUI::CheckBox(config.useMSAA, U"MSAA", Vec2{ 1100, 80 }, 160))
			{
				shadowMap.setConfig(config);
			}

			if (SimpleGUI::CheckBox(config.useMipMap, U"MipMap", Vec2{ 1100, 120 }, 160))
			{
				shadowMap.setConfig(config);
			}

			if (SimpleGUI::RadioButtons(resolutionOption, { U"256x256", U"256x512", U"512x512", U"512x1024", U"1024x1024", U"1024x2048", U"2048x2048", U"2048x4096", U"4096x4096" }, Vec2{ 1100, 160 }, 160))
			{
				config.resolution =
					(resolutionOption == 0) ? Size{ 256, 256 }
					: (resolutionOption == 1) ? Size{ 256, 512 }
					: (resolutionOption == 2) ? Size{ 512, 512 }
					: (resolutionOption == 3) ? Size{ 512, 1024 }
					: (resolutionOption == 4) ? Size{ 1024, 1024 }
					: (resolutionOption == 5) ? Size{ 1024, 2048 }
					: (resolutionOption == 6) ? Size{ 2048, 2048 }
					: (resolutionOption == 7) ? Size{ 2048, 4096 }
				: Size{ 4096, 4096 };
				shadowMap.setConfig(config);
			}

			SimpleGUI::CheckBox(showDepth, U"Show depth", Vec2{ 1100, 520 }, 160);
			SimpleGUI::CheckBox(paused, U"Pause", Vec2{ 1100, 560 }, 160);

			if (showDepth)
			{
				shadowMap.getDepthTexture().resized(512, 512).draw(0, 0, ColorF{ 1 / 100.0, 1 / 10000.0, 0.0 });

				PutText(U"sun {:.1f}, focus {:.1f}, {}x{}"_fmt(light.sunPos, light.focusPos,
					shadowMap.getDepthTexture().width(), shadowMap.getDepthTexture().height()), Arg::bottomRight(506, 510));

				SimpleGUI::Slider(U"viewSize: {:.1f}"_fmt(light.viewSize), light.viewSize, 4.0, 100.0, Vec2{ 180, 640 }, 150.0, 240);

				if (SimpleGUI::Button(U"←", Vec2{ 40, 600 }, 40))
				{
					light.focusPos.x -= 1.0;
				}

				if (SimpleGUI::Button(U"→", Vec2{ 120, 600 }, 40))
				{
					light.focusPos.x += 1.0;
				}

				if (SimpleGUI::Button(U"↑", Vec2{ 80, 560 }, 40))
				{
					light.focusPos.z += 1.0;
				}

				if (SimpleGUI::Button(U"↓", Vec2{ 80, 640 }, 40))
				{
					light.focusPos.z -= 1.0;
				}
			}
		}
	}
}