Open3

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

Ryo SuzukiRyo Suzuki

2D の軌跡の描画

1. 基本

# include <Siv3D.hpp>

void Main()
{
	// 点の基本寿命(秒)
	constexpr double LifeTime = 1.0;

	// 軌跡
	Trail trail{ LifeTime };

	while (System::Update())
	{
		// 軌跡を更新する
		trail.update();

		// 軌跡に点を追加する
		const double t = Scene::Time();
		const Vec2 pos{ (400 + 200 * Math::Cos(t)), (300 + 200 * Math::Sin(t)) };
		const ColorF color{ 0.3, 0.9, 0.6 };
		const double size = 20.0;
		trail.add(pos, color, size);

		// 軌跡を描画する
		trail.draw();
	}
}

2. スケールの制御

# include <Siv3D.hpp>

void Main()
{
	// 点の基本寿命(秒)
	constexpr double LifeTime = 1.0;

	// 点のスケールを、残り寿命によらず常に 1.0 にする
	auto ScaleFunc = [](double) { return 1.0; };

	// 軌跡
	Trail trail{ LifeTime, ScaleFunc };

	while (System::Update())
	{
		// 軌跡を更新する
		trail.update();

		// 軌跡に点を追加する
		const double t = Scene::Time();
		const Vec2 pos{ (400 + 200 * Math::Cos(t)), (300 + 200 * Math::Sin(t)) };
		const ColorF color{ 0.3, 0.9, 0.6 };
		const double size = 20.0;
		trail.add(pos, color, size);

		// 軌跡を描画する
		trail.draw();
	}
}

3. テクスチャの使用

# include <Siv3D.hpp>

void Main()
{
	// 軌跡用のテクスチャ
	const Texture texture{ U"example/particle.png", TextureDesc::Mipped };

	// 点の基本寿命(秒)
	constexpr double LifeTime = 1.0;

	// 軌跡
	Trail trail{ LifeTime };

	while (System::Update())
	{
		// 軌跡を更新する
		trail.update();

		// 軌跡に点を追加する
		const double t = Scene::Time();
		const Vec2 pos{ (400 + 200 * Math::Cos(t)), (300 + 200 * Math::Sin(t)) };
		const ColorF color{ 0.3, 0.9, 0.6 };
		const double size = 20.0;
		trail.add(pos, color, size);

		// テクスチャを指定して軌跡を描画する
		trail.draw(texture);
	}
}

4. プロットの頻度と軌跡の品質

# include <Siv3D.hpp>

void Main()
{
	// 軌跡用のテクスチャ
	const Texture texture{ U"example/particle.png", TextureDesc::Mipped };

	// 点の基本寿命(秒)
	constexpr double LifeTime = 0.5;

	// 軌跡
	Trail trail{ LifeTime };

	// 軌跡のプロット頻度(Hz)。増やすと品質が向上する
	double frequency = 30.0;

	// 再生時間(秒)
	double time = 0.0;

	// 蓄積時間(秒)
	double accumulatedTime = 0.0;

	while (System::Update())
	{
		// 軌跡の更新間隔(秒)
		const double updateInterval = (1.0 / frequency);

		accumulatedTime += Scene::DeltaTime();

		// 蓄積時間が周期を超えていたら、軌跡に点を追加する
		while (updateInterval <= accumulatedTime)
		{
			time += updateInterval;
			accumulatedTime -= updateInterval;

			const Vec2 pos{ (400 + 300 * Math::Sin(time * 8)), (300 + 200 * Math::Sin(time * 12)) };
			const ColorF color{ 1.0, 0.8, 0.4 };
			const double size = 30.0;
			trail.update(updateInterval);
			trail.add(pos, color, size);
		}

		// テクスチャを指定して軌跡を描画する
		trail.draw(texture);

		// 軌跡のプロット頻度を調整するスライダー
		SimpleGUI::Slider(U"{:.0f} Hz"_fmt(frequency), frequency, 15.0, 240.0, Vec2{ 560, 40 }, 80, 120);
	}
}

5. 軌跡のモーション定義クラス

TrailMotion クラスにいくつかの関数やパラメータを制御することで、自動で軌跡をプロットできます。

# include <Siv3D.hpp>

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.15 });
	const Texture texture{ U"example/particle.png", TextureDesc::Mipped };

	// 軌跡のモーション
	TrailMotion trail1 = TrailMotion{}
		.setCenter(500, 300)
		.setPositionFunction([](double t) { return Vec2{ (300 * Sin(t * 6.0)), (200 * Cos(t * 9.0)) }; })
		.setColor(HSV{ 30, 0.5, 1.0 })
		.setSize(20.0);

	TrailMotion trail2 = TrailMotion{}
		.setCenter(500, 300)
		.setPositionFunction([](double t) { return Vec2{ (300 * Sin(t * 2.0)), (160 * Cos(t * 5.0)) }; })
		.setColor(HSV{ 70, 0.5, 1.0 })
		.setSize(20.0);

	TrailMotion trail3 = TrailMotion{ 0.5, 240 }
		.setCenter(500, 300)
		.setPositionFunction([](double t) { return Vec2{ (400 * Sin(t * 3.0)), (120 * Cos(t * 3.0)) }; })
		.setColor(HSV{ 270, 0.5, 1.0 })
		.setSizeFunction([](double t) { return (8.0 + Periodic::Sine0_1(0.15s, t) * 36.0); });

	TrailMotion trail4 = TrailMotion{ 0.5, 240.0, [](double) { return 1.0; }, EaseOutExpo }
		.setCenter(500, 560)
		.setPositionFunction([](double t) { return Vec2{ (240 * Cos(t * 6.0)), (60 * Sin(t * 6.0)) }; })
		.setColor(ColorF{ 0.5, 0.8, 1.0 })
		.setSize(24.0);

	Array<TrailMotion> electroTrails(4,
		TrailMotion{ 0.5, 240 }
			.setCenter(0, 0)
			.setColor(ColorF{ 1.0 })
			.setSize(12.0));

	{
		const double offsets[4] = { Random(1.0, 2.0), Random(1.0, 2.0), Random(1.0, 2.0), Random(1.0, 2.0) };

		for (size_t i = 0; i < electroTrails.size(); ++i)
		{
			electroTrails[i].setPositionFunction(
				[=](double t)
				{
					double x = ((80.0 + i * 8) + Random(-4.0, 4.0)) * Math::Cos(offsets[i] + t * (4.0 + i * (1 + offsets[i])));
					double y = ((80.0 + i * 8) + Random(-4.0, 4.0)) * Math::Sin(offsets[i] + t * (4.0 + i * (1 + offsets[i])));
					return Vec2{ x, y };
				}
			);
		}
	}

	double timeScale = 1.0;
	bool wireframe = false;

	while (System::Update())
	{
		for (int32 y = 0; y < 36; ++y)
		{
			for (int32 x = 0; x < 64; ++x)
			{
				if (IsEven(y + x))
				{
					Rect{ (x * 20), y * 20, 20 }.draw(ColorF{ 0.11 });
				}
			}
		}

		{
			const double deltaTime = (Scene::DeltaTime() * timeScale);
			trail1.update(deltaTime);
			trail2.update(deltaTime);
			trail3.update(deltaTime);
			trail4.update(deltaTime);

			for (auto& trail : electroTrails)
			{
				trail.update(deltaTime);
			}
		}

		if (wireframe)
		{
			const ScopedRenderStates2D blend{ RasterizerState::WireframeCullNone };
			const ScopedColorAdd2D colorAdd{ ColorF{ 1.0 } };
			trail1.draw();
			trail2.draw();
			trail3.draw();
			trail4.draw();

			{
				const Transformer2D transformer{ Mat3x2::Translate(Cursor::PosF()) };

				for (const auto& trail : electroTrails)
				{
					trail.draw();
				}
			}
		}
		else
		{
			trail1.draw(texture);
			trail2.draw(texture);
			trail3.draw(texture);
			trail4.draw();

			{
				const Transformer2D transformer{ Mat3x2::Translate(Cursor::PosF()) };

				for (const auto& trail : electroTrails)
				{
					trail.draw(texture);
				}
			}
		}

		RoundRect{ 945, 20, 300, 120, 8 }.drawShadow({ 4, 4 }, 8, 3).draw();
		SimpleGUI::CheckBox(wireframe, U"wireframe", Vec2{ 960, 40 });
		SimpleGUI::Slider(U"speed x{:.2f}"_fmt(timeScale), timeScale, 0.01, 1.0, Vec2{ 960, 80 }, 140, 140);
	}
}

6. 軌跡によるエフェクトのサンプル

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

# include <Siv3D.hpp>

struct EffectCircle
{
	ColorF color;
	bool additiveBlend = false;
	Array<TrailMotion> trails;

	void update()
	{
		for (auto& trail : trails)
		{
			trail.update();
		}
	}

	void draw(const Vec2& center, double size, const Texture& texture) const
	{
		const Transformer2D transform{ Mat3x2::Scale(size / 80.0).translated(center) };
		const Circle circle{ 80 };
		circle.drawFrame(0, 12, ColorF{ 0.0, 0.2 }, ColorF{ 0.0, 0.0 })
			.draw((color + ColorF{ 0.05 }), (color - ColorF{ 0.05 }));
		{
			const ScopedRenderStates2D blend{ BlendState::Additive };
			circle.drawFrame(12, 0, (color * 0.8));
		}

		{
			const ScopedRenderStates2D blend{ additiveBlend ? BlendState::Additive : BlendState::Default2D };
			for (const auto& trail : trails)
			{
				trail.draw(texture);
			}
		}
	}
};

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.6 });
	const Texture texture{ U"example/particle.png", TextureDesc::Mipped };
	const double offsets[3] = { Random(1.0, 2.0), Random(1.0, 2.0), Random(1.0, 2.0) };
	double size = 120.0;
	bool wireframe = false;

	EffectCircle pyro{ ColorF{ 0.95, 0.4, 0.3 } };
	for (int32 i = 0; i < 3; ++i)
	{
		const HSV hsv{ 10.0, 0.67, (0.8 + i * 0.1) };
		pyro.trails << TrailMotion{}
			.setScaleFunction([](double t)
			{
				return ((0.9 < t) ? EaseOutBack((1.0 - t) * 10.0) : 1.0);
			})
			.setPositionFunction([i, &offsets](double t)
			{
				double x = ((80 + (i + 1) * 4)) * -Math::Cos(offsets[i] + t * (4.0 + i * (1 + offsets[i])));
				double y = ((80 + (i + 1) * 4)) * Math::Sin(offsets[i] + t * (4.0 + i * (1 + offsets[i])));
				return Vec2{ x, y };
			})
				.setSizeFunction([](double t)
				{
						return (6.0 + Periodic::Sine0_1(0.2s, t) * 28.0);
				})
				.setColor(hsv)
					.setFrequency(240.0);
	}

	EffectCircle electro{ ColorF{ 0.5, 0.3, 0.9 }, true };
	for (int32 i = 0; i < 3; ++i)
	{
		electro.trails << TrailMotion{}
		.setAlphaFunction([](double) { return 1.0; })
			.setPositionFunction([i, &offsets](double t)
			{
				double x = ((80 + (i + 1) * 4) + Random(-4.0, 4.0)) * -Math::Cos(offsets[i] + t * (4.0 + i * (1 + offsets[i])));
				double y = ((80 + (i + 1) * 4) + Random(-4.0, 4.0)) * Math::Sin(offsets[i] + t * (4.0 + i * (1 + offsets[i])));
				return Vec2{ x, y };
			})
			.setSize(12.0)
				.setColor(ColorF{ electro.color, 0.2 })
				.setFrequency(300.0);
	}

	EffectCircle cryo{ ColorF{ 0.5, 0.8, 1.0 }, true };
	for (int32 i = 0; i < 3; ++i)
	{
		const HSV hsv{ 220, 0.67, (0.9 + i * 0.05) };
		cryo.trails << TrailMotion{}
			.setScaleFunction([](double t)
			{
				return (0.5 - AbsDiff(t, 0.5));
			})
			.setPositionFunction([i, &offsets](double t)
			{
				double x = ((80 + i * 4)) * -Math::Cos(offsets[i] + t * (5.0 + i * (1 + offsets[i])));
				double y = ((80 + i * 4)) * Math::Sin(offsets[i] + t * (5.0 + i * (1 + offsets[i])));
				return Vec2{ x, y };
			})
				.setSizeFunction([](double t)
				{
						return (18.0 + Periodic::Sine0_1(0.2s, t) * 42.0);
				})
				.setColor(hsv);
	}

	EffectCircle anemo{ ColorF{ 0.4, 0.8, 0.4 } };
	for (int32 i = 0; i < 3; ++i)
	{
		const HSV hsv{ 120, 0.5, (0.7 + i * 0.1) };
		anemo.trails << TrailMotion{}
			.setScaleFunction([](double t)
			{
				return (0.5 - AbsDiff(t, 0.5));
			})
			.setPositionFunction([i, &offsets](double t)
			{
				double x = ((72 + i * 4)) * -Math::Cos(offsets[i] + t * (4.0 + i * (1 + offsets[i])) * 3.0);
				double y = ((72 + i * 4)) * Math::Sin(offsets[i] + t * (4.0 + i * (1 + offsets[i])) * 2.0);
				return Vec2{ x, y };
			})
				.setSizeFunction([](double t)
				{
						return (16.0 + Periodic::Sine0_1(0.2s, t) * 48.0);
				})
				.setColor(hsv)
					.setFrequency(240.0);
	}

	while (System::Update())
	{
		for (int32 y = 0; y < 36; ++y)
		{
			for (int32 x = 0; x < 64; ++x)
			{
				if (IsEven(y + x))
				{
					Rect{ (x * 20), y * 20, 20 }.draw(ColorF{ 0.55 });
				}
			}
		}

		pyro.update();
		electro.update();
		cryo.update();
		anemo.update();
		{
			const ScopedRenderStates2D rs{ wireframe ? RasterizerState::WireframeCullBack : RasterizerState::Default2D };
			const ScopedColorAdd2D add{ wireframe ? ColorF{ 1.0, 1.0 } : ColorF{ 0.0, 0.0 } };
			pyro.draw(Vec2{ 250, 200 }, size, texture);
			electro.draw(Vec2{ 250, 520 }, size, texture);
			cryo.draw(Vec2{ 650, 200 }, size, texture);
			anemo.draw(Vec2{ 650, 520 }, size, texture);
		}

		SimpleGUI::Slider(U"size", size, 0.0, 200.0, Vec2{ 880, 80 }, 80, 100);
		SimpleGUI::CheckBox(wireframe, U"wireframe", Vec2{ 880, 120 }, 180);
	}
}
Ryo SuzukiRyo Suzuki

9 スライスの描画

1. 基本

下記の画像ファイルをプロジェクトに配置する

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.7 });
	constexpr int32 CellSize = 20;

	NinePatch ninePatch{ U"patch0.png", 64 };

	while (System::Update())
	{
		for (int32 y = 0; y < (Scene::Height() / CellSize); ++y)
		{
			for (int32 x = 0; x < (Scene::Width() / CellSize); ++x)
			{
				if (IsEven(y + x))
				{
					Rect{ (x * CellSize), (y * CellSize), CellSize }.draw(ColorF{ 0.65 });
				}
			}
		}

		ninePatch.draw(Rect{ 40, 40, 100, 100 }, 0.5);

		ninePatch.draw(Rect{ 40, 160, 160, 160 }, 0.125);

		ninePatch.draw(Rect{ 220, 160, 160, 160 }, 0.25);

		ninePatch.draw(Rect{ 400, 160, 160, 160 }, 0.5);

		ninePatch.draw(Rect{ 580, 160, 160, 160 }, 1.0);

		ninePatch.draw(Rect{ 40, 340, 400, 200 }, 0.5);
	}
}

2. 原理の説明

# include <Siv3D.hpp>

Texture MakeExample9PatchTexture()
{
	// 描画された最大のアルファ成分を保持するブレンドステートを作成する関数
	auto MakeBlendState = []()
		{
			BlendState blendState = BlendState::Default2D;
			blendState.srcAlpha = Blend::SrcAlpha;
			blendState.dstAlpha = Blend::DestAlpha;
			blendState.opAlpha = BlendOp::Max;
			return blendState;
		};

	MSRenderTexture renderTexture{ Size{ 128, 128 }, ColorF{ 0.5, 0.0 } };
	{
		// レンダーターゲットを renderTexture に変更する
		const ScopedRenderTarget2D target{ renderTexture };

		// 描画された最大のアルファ成分を保持するブレンドステート
		const ScopedRenderStates2D blend{ MakeBlendState() };

		Triangle{ 32, 0, 32, 32, 0, 32 }.draw(ColorF{ 1.0 });
		Triangle{ 96, 0, 128, 32, 96, 32 }.draw(ColorF{ 1.0 });
		Triangle{ 32, 96, 32, 128, 0, 96 }.draw(ColorF{ 1.0 });
		Triangle{ 96, 96, 128, 96, 96, 128 }.draw(ColorF{ 1.0 });

		for (int32 x = 0; x < 4; ++x)
		{
			Rect{ (32 + x * 16), (x % 2 * 16), 16 }.draw(HSV{ 20.0, 0.4, 1.0 });
			Rect{ (32 + x * 16), ((x + 1) % 2 * 16), 16 }.draw(HSV{ 40.0, 0.4, 1.0 });

			Rect{ (32 + x * 16), (96 + x % 2 * 16), 16 }.draw(HSV{ 60.0, 0.4, 1.0 });
			Rect{ (32 + x * 16), (96 + (x + 1) % 2 * 16), 16 }.draw(HSV{ 80.0, 0.4, 1.0 });
		}

		for (int32 y = 0; y < 4; ++y)
		{
			Rect{ (y % 2 * 16), (32 + y * 16), 16 }.draw(HSV{ 100.0, 0.4, 1.0 });
			Rect{ ((y + 1) % 2 * 16), (32 + y * 16), 16 }.draw(HSV{ 120.0, 0.4, 0.9 });

			Rect{ (96 + y % 2 * 16), (32 + y * 16), 16 }.draw(HSV{ 140.0, 0.4, 1.0 });
			Rect{ (96 + (y + 1) % 2 * 16), (32 + y * 16), 16 }.draw(HSV{ 160.0, 0.4, 0.9 });
		}

		for (int32 y = 0; y < 4; ++y)
		{
			for (int32 x = 0; x < 4; ++x)
			{
				Rect{ (32 + x * 16), (32 + y * 16), 16 }.draw(HSV{ (IsEven(x + y) ? 180.0 : 200.0), 0.4, 1.0 });
			}
		}
	}
	Graphics2D::Flush();
	renderTexture.resolve();

	return renderTexture;
}

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	const Texture ninePatchTexture = MakeExample9PatchTexture();
	constexpr int32 TextureCornerSize = 32;

	const NinePatch ninePatch{ ninePatchTexture, TextureCornerSize, NinePatch::Style::Default() };
	const NinePatch ninePatchTiled{ ninePatchTexture, TextureCornerSize, NinePatch::Style::TileAll() };

	bool repeat = true;
	bool linearFilter = true;
	size_t textureScaleIndex = 2;
	double width = 400.0;
	double height = 300.0;

	while (System::Update())
	{
		RoundRect{ 40, 40, 500, 180, 10 }.draw(ColorF{ 0.25 });
		font(U"9-patch texture").draw(30, Vec2{ 60, 60 }, ColorF{ 0.98 });
		ninePatchTexture.draw(Vec2{ 320, 70 });

		const double textureScale = Math::Exp2(textureScaleIndex - 2.0);
		const TextureFilter textureFilter = (linearFilter ? TextureFilter::Linear : TextureFilter::Nearest);

		if (repeat)
		{
			ninePatchTiled.draw(RectF{ 40, 240, width, height }, textureScale, textureFilter);
		}
		else
		{
			ninePatch.draw(RectF{ 40, 240, width, height }, textureScale, textureFilter);
		}

		SimpleGUI::CheckBox(repeat, U"Tiled", Vec2{ 560, 40 }, 200);
		SimpleGUI::CheckBox(linearFilter, U"LinearFilter", Vec2{ 560, 80 }, 200);
		SimpleGUI::RadioButtons(textureScaleIndex, { U"0.25", U"0.5", U"1.0", U"2.0" }, Vec2{ 560, 120 }, 200);
		SimpleGUI::Slider(width, 64.0, 480.0, Vec2{ 560, 280 }, 200);
		SimpleGUI::Slider(height, 64.0, 320.0, Vec2{ 560, 320 }, 200);
	}
}

3. 発展

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

下記の 5 つの画像ファイルをプロジェクトに配置する

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.7 });
	constexpr int32 CellSize = 20;
	const Font font{ FontMethod::MSDF, 48, U"example/font/RocknRoll/RocknRollOne-Regular.ttf" };
	Stopwatch stopwatch{ StartImmediately::Yes };

	Array<NinePatch> ninePatches =
	{
		NinePatch{ U"patch0.png", 64 },
		NinePatch{ U"patch1.png", 64 },
		NinePatch{ U"patch2.png", 64 },
		NinePatch{ U"patch3.png", 30 },
		NinePatch{ U"patch4.png", 8 },
	};

	size_t index = 0;
	double width = 640;
	double height = 220;

	while (System::Update())
	{
		for (int32 y = 0; y < (Scene::Height() / CellSize); ++y)
		{
			for (int32 x = 0; x < (Scene::Width() / CellSize); ++x)
			{
				if (IsEven(y + x))
				{
					Rect{ (x * CellSize), (y * CellSize), CellSize }.draw(ColorF{ 0.65 });
				}
			}
		}

		{
			const Transformer2D transformer(Mat3x2::Translate(80, 40));

			ninePatches[index].getTexture().draw();
			const auto& patchSize = ninePatches[index].getPatchSize();

			patchSize.topLeftRect().drawFrame(1, 0);
			patchSize.topRect().drawFrame(1, 0);
			patchSize.topRightRect().drawFrame(1, 0);
			patchSize.leftRect().drawFrame(1, 0);
			patchSize.centerRect().drawFrame(1, 0);
			patchSize.rightRect().drawFrame(1, 0);
			patchSize.bottomLeftRect().drawFrame(1, 0);
			patchSize.bottomRect().drawFrame(1, 0);
			patchSize.bottomRightRect().drawFrame(1, 0);
		}

		if (SimpleGUI::RadioButtons(index, { U"patch0", U"patch1", U"patch2", U"patch3", U"patch4" }, Vec2{ 460, 40 }, 120))
		{
			stopwatch.restart();
		}

		SimpleGUI::Slider(width, 100.0, 800.0, Vec2{ 600, 40 }, 120);
		SimpleGUI::Slider(height, 100.0, 320.0, Vec2{ 600, 80 }, 120);

		const RectF rect{ ((800 - width) / 2), 280, width, height };

		if (InRange((int32)index, 0, 2))
		{
			ninePatches[index].draw(rect, 0.5);
		}
		else if (index == 3)
		{
			ninePatches[index].draw(rect, 2.0, TextureFilter::Nearest);
		}
		else if (index == 4)
		{
			ninePatches[index].draw(rect, 8.0, TextureFilter::Nearest);
		}

		font(U"Hello, Siv3D!"_sv.substr(0, stopwatch.ms() / 30)).draw(36, rect.stretched(-60, -48), ColorF{ 0.98 });
	}
}
Ryo SuzukiRyo Suzuki

目標に追従するシンプルな 3D カメラ

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

# include <Siv3D.hpp>

void Main()
{
	Window::Resize(1280, 720);
	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

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

	constexpr double Speed = 4.0;
	Vec3 currentPos{ 0, 1, 0 };
	Vec3 targetPos{ 0, 1, 0 };
	Vec3 velocity{ 0, 0, 0 };

	constexpr double FollowDistance = 15.0;
	double followHeight = 8.0;
	double followDirection = 0.0_deg;
	SimpleFollowCamera3D camera{ renderTexture.size(), 30_deg, currentPos, 0.0_deg, FollowDistance, followHeight };

	while (System::Update())
	{
		const double deltaTime = Scene::DeltaTime();

		// 追従の調整
		{
			if (KeyLeft.pressed())
			{
				followDirection -= (90_deg * deltaTime);
			}

			if (KeyRight.pressed())
			{
				followDirection += (90_deg * deltaTime);
			}

			if (KeyDown.pressed())
			{
				followHeight = Min((followHeight + 4.0 * deltaTime), 20.0);
			}

			if (KeyUp.pressed())
			{
				followHeight = Max((followHeight - 4.0 * deltaTime), 0.0);
			}
		}

		// キャラクターの移動
		{
			if (KeyA.pressed())
			{
				targetPos.x -= (Speed * deltaTime);
			}

			if (KeyD.pressed())
			{
				targetPos.x += (Speed * deltaTime);
			}

			if (KeyW.pressed())
			{
				targetPos.z += (Speed * deltaTime);
			}

			if (KeyS.pressed())
			{
				targetPos.z -= (Speed * deltaTime);
			}

			currentPos = Math::SmoothDamp(currentPos, targetPos, velocity, 0.12);
		}

		if (KeyC.down())
		{
			currentPos = targetPos = Vec3{ 0, 1, 0 };
			velocity = Vec3{ 0, 0, 0 };
			followHeight = 8.0;
			followDirection = 0.0_deg;
			camera.jumpToTarget(currentPos, followDirection);
		}

		// カメラの更新
		{
			camera.setTarget(currentPos, followDirection);
			camera.setFollowOffset(FollowDistance, followHeight);
			camera.update(0.15);
			Graphics3D::SetCameraTransform(camera);
		}

		// 3D 描画
		{
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			Plane{ 64 }.draw(uvChecker);
			Box{ currentPos, 2 }.draw(ColorF{ 0.8, 0.6, 0.4 }.removeSRGBCurve());
		}

		// 3D シーンを 2D シーンに描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}

		RoundRect{ 20, 20, 680, 200, 10 }.draw(ColorF{ 0.0, 0.6 });
		font(U"キャラクター位置: {:.2f}"_fmt(currentPos)).draw(24, Vec2{ 40, 40 });
		font(U"追従方向: {:.1f}°"_fmt(Math::ToDegrees(Math::NormalizeAngle(followDirection)))).draw(24, Vec2{ 40, 80 });
		font(U"追従距離: {:.2f}"_fmt(FollowDistance)).draw(24, Vec2{ 240, 80 });
		font(U"追従高さ: {:.2f}"_fmt(followHeight)).draw(24, Vec2{ 440, 80 });
		font(U"追従オフセット: {:.2f}"_fmt(camera.getFollowOffset())).draw(24, Vec2{ 40, 120 });
		font(U"移動: WASD, 追従方向: ←→, 追従高さ: ↑↓ リセット: C"_fmt(camera.getFollowOffset())).draw(24, Vec2{ 40, 160 });
	}
}