【C++/OpenSiv3D】泡のアニメーション Texture

![Siv3D-App-2019-12-01-17-51-44.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30255/72a34db7-d08d-b283-95aa-c27d94938272.gif) ![Siv3D-App-2019-12-01-19-25-42.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30255/64edb465-86bd-ec09-a17f-72fff1a50016.gif)

泡が上昇するアニメーションを `RenderTexture` で生成し、そのテクスチャを UI やゲームの要素に使うと、面白い表現ができます。
この記事では、泡の生成、アニメーションの更新、`Texture` の取得までを 4 行で書けるようにする `BubbleTexture` クラスを実装します。（対象バージョン: OpenSiv3D v0.4.2 以上）

## `BubbleTexture` クラスのソースコード
コピーすれば、すぐに使えます。

```C++
class BubbleTexture
{
private:

	// 泡を描画する内部テクスチャ
	MSRenderTexture m_texture;

	// 泡を表現する Circle の配列
	Array<Circle> m_bubbles;

	// 泡の発生レートの最大値（個/秒）
	static constexpr double MaxSpawnRate = 300.0;

	// 泡のスピードの最小値（ピクセル/秒）
	static constexpr double MinSpeed = 10.0;

	// 泡の発生レート（個/秒）
	double m_spawnRate = 10.0;

	// 泡のスピード（ピクセル/秒）
	double m_speed = 40.0;

	// 泡の最小サイズ
	double m_minSize = 0.0;

	// 泡の最大サイズ
	double m_maxSize = 0.0;

	// 泡発生更新用のカウンタ
	double m_timeAccum = 0.0;

public:

	BubbleTexture() = default;

	// コンストラクタ
	BubbleTexture(const Size& size, double spawnRate, double speed, double minSize, double maxSize)
		: m_texture(size)
		, m_spawnRate(Clamp(spawnRate, 0.0, MaxSpawnRate))
		, m_speed(Max(speed, MinSpeed))
		, m_minSize(minSize)
		, m_maxSize(maxSize)
	{
		// prewarm
		const double prewarmTime = (size.y + m_maxSize * 2) / speed;
		const double prewarmStepTime = 1.0 / 60.0;

		for (int32 i = 0; i < (prewarmTime / prewarmStepTime); ++i)
		{
			update(prewarmStepTime);
		}
	}

	// 泡の更新
	void update(double deltaTime)
	{
		// 泡の移動
		{
			const double speedBase = ((m_minSize + m_maxSize) * 0.5);

			for (auto& bubble : m_bubbles)
			{
				bubble.y -= deltaTime * m_speed * (bubble.r / speedBase);
			}
			m_bubbles.remove_if([maxSize = m_maxSize](const Circle& c) { return c.y < -maxSize; });
		}

		// 泡の発生
		{
			m_timeAccum += deltaTime;

			const double spawnTime = 1.0 / m_spawnRate;

			while (m_timeAccum > spawnTime)
			{
				const RectF spawnArea(-m_maxSize, m_texture.height() + m_maxSize, m_texture.width() + (m_maxSize * 2), m_maxSize);

				m_bubbles.emplace_back(RandomVec2(spawnArea), Random(m_minSize, m_maxSize));

				m_timeAccum -= spawnTime;
			}
		}
	}

	// 泡を内部テクスチャに描画
	void render(const ColorF& backgroundColor)
	{
		m_texture.clear(backgroundColor);
		{
			ScopedRenderTarget2D rt(m_texture);
			ScopedRenderStates2D blend(BlendState::Additive);

			for (const auto& bubble : m_bubbles)
			{
				const double a = EaseInQuad(0.15 + bubble.y / m_texture.height() * 0.45);
				bubble.draw(ColorF(1.0, a));
			}
		}
		Graphics2D::Flush();
		m_texture.resolve();
	}

	// 内部テクスチャを取得
	const Texture& getTexture() const
	{
		return m_texture;
	}
};
```

## 使用例 1

![Siv3D-App-2019-12-01-17-51-44.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30255/72a34db7-d08d-b283-95aa-c27d94938272.gif)

```C++
# include <Siv3D.hpp> // OpenSiv3D v0.4.2

/* ここに BubbleTexture クラス */

void Main()
{
	Scene::SetBackground(ColorF(0.4));

	const Font font(40, U"example/font/LogoTypeGothic/LogoTypeGothic.otf");

	constexpr Size size(240, 80);

	constexpr RoundRect button(280, 400, size, 20);

	BubbleTexture bubbleTexture(size, 10, 40, 2, 6);

	while (System::Update())
	{
		// 泡の更新
		{
			bubbleTexture.update(Scene::DeltaTime());
			bubbleTexture.render(ColorF(0.1, 0.3, 0.6));
		}

		// 描画
		{
			// 背景
			Circle(Scene::Center(), 800).draw(ColorF(1.0), ColorF(0.1, 0.3, 0.6));

			// ボタン
			button(bubbleTexture.getTexture())
				.draw()
				.drawFrame(2, 0, ColorF(1.0, 0.5));

			// ボタンの文字
			font(U"はじめる").drawAt(button.center());

			// カーソル
			if (button.mouseOver())
			{
				Cursor::RequestStyle(CursorStyle::Hand);
			}
		}
	}
}
```

## 使用例 2

![Siv3D-App-2019-12-01-19-25-42.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30255/64edb465-86bd-ec09-a17f-72fff1a50016.gif)

```C++
# include <Siv3D.hpp> // OpenSiv3D v0.4.2

/* ここに BubbleTexture クラス */

void Main()
{
	Scene::SetBackground(ColorF(0.4));

	const Texture texture(Emoji(U"🐧"));

	constexpr Size size(180, 240);

	constexpr RoundRect rect(310, 200, size, 90);

	BubbleTexture bubbleTexture(Size(size.x * 2, size.y), 50, 90, 1, 4);

	while (System::Update())
	{
		// 泡の更新
		{
			bubbleTexture.update(Scene::DeltaTime());
			bubbleTexture.render(ColorF(0.0));
		}

		// 描画
		{
			Circle(Scene::Center(), 800).draw(ColorF(0.3, 0.0, 0.3), ColorF(0.0));

			Ellipse(rect.rect.bottomCenter(), rect.w / 4, rect.w / 12)
				.draw(ColorF(0.6));

			rect.draw(ColorF(0.1, 0.8, 0.2, 0.6));
			
			{
				ScopedRenderStates2D blend(BlendState::Additive);
				rect(bubbleTexture.getTexture()(0, 0, size)).draw();
			}

			texture.drawAt(rect.center().movedBy(0, Periodic::Sine0_1(5s) * 10));

			rect.draw(ColorF(0.1, 0.8, 0.2, 0.3));

			{
				ScopedRenderStates2D blend(BlendState::Additive);
				rect(bubbleTexture.getTexture()(size.x, 0, size)).draw();
			}

			rect.drawFrame(0.5, 0, ColorF(0.0, 0.5));

			Ellipse(rect.rect.topCenter().movedBy(0, 8), rect.w / 4, rect.w / 12)
				.draw(ColorF(0.6));
		}
	}
}
```