Open13

Siv3D Advent Calendar 2024 ネタ帳

Ryo SuzukiRyo Suzuki

物理演算でボールを転がして、ゴールに到達したかを判定する

  • P2Body から元になった基本図形を取得し、通常の図形どうしのあたり判定をするのがポイントです。
# include <Siv3D.hpp> // Siv3D v0.6.15

void Main()
{
	// ウィンドウを 1280x720 のサイズに変更する
	Window::Resize(1280, 720);

	// 背景色を設定する
	Scene::SetBackground(ColorF{ 0.2, 0.5, 0.8 });

	// フォント
	const Font font = Font{ FontMethod::MSDF, 48, Typeface::Heavy }.setBufferThickness(4);

	// 2D 物理演算のシミュレーションステップ(秒)
	constexpr double StepTime = (1.0 / 200.0);

	// 2D 物理演算のシミュレーション蓄積時間(秒)
	double accumulatedTime = 0.0;

	// 2D 物理演算のワールド
	P2World world;

	// 床
	Array<P2Body> grounds;
	grounds << world.createRect(P2Static, Vec2{ 640, 600 }, SizeF{ 1000, 10 });
	grounds << world.createLine(P2Static, Vec2{ 300, 500 }, Line{ -200, -20, 200, 20 });

	// ボール
	const P2Body ball = world.createCircle(P2Dynamic, Vec2{ 200, 400 }, 20);

	// ゴール判定領域
	const RectF goalArea{ 980, 500, 40, 100 };

	// ゴールしたか
	bool goal = false;

	while (System::Update())
	{
		// ゴール後は物理演算を停止する
		if (not goal)
		{
			for (accumulatedTime += Scene::DeltaTime(); StepTime <= accumulatedTime; accumulatedTime -= StepTime)
			{
				// 2D 物理演算のワールドを StepTime 秒進める
				world.update(StepTime);
			}
		}

		{
			// createCircle() で作成したボールから P2Circle を取得できる。
			const P2Circle& circleShape = static_cast<const P2Circle&>(ball.shape(0));

			// P2Circle から Circle を取得できるので、RectF とのあたり判定が可能
			if (circleShape.getCircle().intersects(goalArea))
			{
				// ゴール領域とボールが重なったらゴール
				goal = true;
			}
		}

		// 地面を描画する
		for (const auto& ground : grounds)
		{
			ground.draw();
		}

		// ボールを描画する
		ball.draw();

		// ゴール判定領域を描画する
		goalArea.draw(ColorF{ 0.2, 1.0, 0.6, 0.5 });

		// ゴールしたら SUCCESS! と表示する
		if (goal)
		{
			font(U"SUCCESS!")
				.drawAt(TextStyle::OutlineShadow(0.25, ColorF{ 0.11 }, Vec2{ 2, 2 }, ColorF{ 0.11 }),
					90, Scene::Center(), ColorF{ 1.0 });
		}
	}
}
Ryo SuzukiRyo Suzuki

フォント描画で手書き感を出す

https://x.com/Reputeless/status/1860891492702920739

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

struct PointsGlyph : OutlineGlyph
{
	Array<Array<Vec2>> points;

	PointsGlyph() = default;

	PointsGlyph(const OutlineGlyph& outlineGlyph, double distance)
		: OutlineGlyph{ outlineGlyph }
	{
		for (const auto& ring : outlineGlyph.rings)
		{
			const double length = ring.calculateLength();
			const int32 count = static_cast<int32>((length / distance) + 1);
			distance = (length / count);

			Array<Vec2> densifiedPoints;

			for (size_t i = 0; i < count; ++i)
			{
				const double t = (i * distance);
				const Vec2 point = ring.calculatePointFromOrigin(t);
				densifiedPoints << point;
			}

			points << densifiedPoints;
		}
	}
};

struct SketchGlyph : PointsGlyph
{
	struct Style
	{
		double pointDistance = 2.5;

		double strokeRandomness = 0.12;

		double angleThreshold = 166_deg;

		double stretchFactor = 0.45;

		double shiftFactor = 0.22;
	};

	Array<Line> lines;

	SketchGlyph() = default;

	SketchGlyph(const OutlineGlyph& outlineGlyph, const Style& style)
		: PointsGlyph{ outlineGlyph, style.pointDistance }
	{
		for (const auto& densifiedPoints : points)
		{
			for (size_t i = 0; i < densifiedPoints.size(); ++i)
			{
				std::array<Vec2, 10> localPoints{};

				for (size_t k = 0; k < localPoints.size(); ++k)
				{
					localPoints[k] = densifiedPoints[((i + k) % densifiedPoints.size())]
						.movedBy(RandomVec2(style.pointDistance * style.strokeRandomness));
				}

				size_t targetIndex = 1;

				for (; targetIndex < (localPoints.size() - 1); ++targetIndex)
				{
					const Vec2 v0 = (localPoints[0] - localPoints[targetIndex]);
					const Vec2 v1 = (localPoints[targetIndex + 1] - localPoints[targetIndex]);

					if (v0.getAngle(v1) < style.angleThreshold)
					{
						break;
					}
				}

				const Vec2 p0 = localPoints[0];
				const Vec2 p1 = localPoints[targetIndex];
				const Line line = Line{ p0, p1 }.stretched(Random(style.pointDistance * targetIndex * style.stretchFactor));
				lines << line.movedBy(line.normal().withLength(Random(style.pointDistance * style.shiftFactor) + Random(style.pointDistance * style.shiftFactor)));

				if (4 < targetIndex)
				{
					i += 2;
				}
			}
		}
	}
};

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.99, 0.97, 0.95 });

	const Font fontOutline{ 144, Typeface::Bold };
	const Font fontSDF{ FontMethod::SDF, 12, Typeface::Bold };
	const Font fontBitmap{ FontMethod::Bitmap, 22, Typeface::Bold };

	TextEditState textEditState{ U"Siv3Dあいう漢字" };
	Array<SketchGlyph> glyphs;
	SketchGlyph::Style style;
	double baseThickness = 1.7;
	double thicknessAttenuationFactor = 0.02;

	while (System::Update())
	{
		Vec2 penPos{ 60, 120 };
		fontSDF(textEditState.text).draw(fontOutline.fontSize(), penPos, ColorF{ 1.0, 0.7, 0.8 });
		fontBitmap(textEditState.text).draw(fontOutline.fontSize(), penPos, ColorF{ 1.0, 0.7, 0.8, 0.8 });

		for (const auto& glyph : glyphs)
		{
			const Transformer2D tr{ Mat3x2::Translate(penPos + glyph.getOffset()) };

			for (const auto& line : glyph.lines)
			{
				line.draw((baseThickness - line.length() * thicknessAttenuationFactor), ColorF{ 0.3, 0.2, 0.1, 0.3 });
			}

			penPos.x += glyph.xAdvance;
		}

		bool hasChanged = false;

		if (SimpleGUI::TextBox(textEditState, Vec2{ 60, 340 }, 200)
			|| (textEditState.text && (not glyphs)))
		{
			hasChanged = true;
		}

		if (SimpleGUI::Slider(U"pointDistance: {:.1f}"_fmt(style.pointDistance), style.pointDistance, 0.2, 8.0, Vec2{ 60, 380 }, 240, 200))
		{
			hasChanged = true;
		}

		if (SimpleGUI::Slider(U"strokeRandomness: {:.2f}"_fmt(style.strokeRandomness), style.strokeRandomness, 0.0, 0.5, Vec2{ 60, 420 }, 240, 200))
		{
			hasChanged = true;
		}

		if (SimpleGUI::Slider(U"angleThreshold: {:.0f}"_fmt(Math::ToDegrees(style.angleThreshold)), style.angleThreshold, 90_deg, 180_deg, Vec2{ 60, 460 }, 240, 200))
		{
			hasChanged = true;
		}

		if (SimpleGUI::Slider(U"stretchFactor: {:.2f}"_fmt(style.stretchFactor), style.stretchFactor, 0.0, 1.0, Vec2{ 60, 500 }, 240, 200))
		{
			hasChanged = true;
		}

		if (SimpleGUI::Slider(U"shiftFactor: {:.2f}"_fmt(style.shiftFactor), style.shiftFactor, 0.0, 1.0, Vec2{ 60, 540 }, 240, 200))
		{
			hasChanged = true;
		}

		if (SimpleGUI::Slider(U"baseThickness: {:.1f}"_fmt(baseThickness), baseThickness, 0.5, 5.0, Vec2{ 60, 580 }, 240, 200))
		{
			hasChanged = true;
		}

		if (SimpleGUI::Slider(U"thicknessAttenuationFactor: {:.2f}"_fmt(thicknessAttenuationFactor), thicknessAttenuationFactor, 0.0, 0.5, Vec2{ 60, 620 }, 360, 200))
		{
			hasChanged = true;
		}

		if (hasChanged)
		{
			glyphs = fontOutline.renderOutlines(textEditState.text, CloseRing::Yes).map([&](const OutlineGlyph& outlineGlyph) { return SketchGlyph{ outlineGlyph, style }; });
		}
	}
}

Ryo SuzukiRyo Suzuki

獲得したアイテムを輝かしく見せる

  • 上下左右方向にシフトした描画で輪郭を足し、アイテムを強調します。
  • Circle::draw()Circle::drawPie() で輝いている背景を作ります。
# include <Siv3D.hpp> // Siv3D v0.6.15

namespace EffectLib
{
	void DrawWithOutline(const Texture& texture, const Vec2& pos, double offset, const ColorF& outlineColor)
	{
		{
			const ScopedColorAdd2D colorAdd{ ColorF{ outlineColor, 0.0 } };
			texture.drawAt(pos.movedBy(0, -offset), ColorF{ 0.0 });
			texture.drawAt(pos.movedBy(-offset, 0), ColorF{ 0.0 });
			texture.drawAt(pos.movedBy(0, offset), ColorF{ 0.0 });
			texture.drawAt(pos.movedBy(offset, 0), ColorF{ 0.0 });
		}

		texture.drawAt(pos);
	}

	void DrawGlowEffect1(const Vec2& center, double radius, const ColorF& color)
	{
		Circle{ center, (radius * 0.4) }.draw(color, ColorF{ color, 0.0 });
		Circle{ center, (radius * 0.6) }.draw(color, ColorF{ color, 0.0 });
		Circle{ center, (radius * 0.8) }.draw(color, ColorF{ color, 0.0 });
		Circle{ center, radius }.draw(color, ColorF{ color, 0.0 });
	}

	void DrawGlowEffect2(const Vec2& center, double radius, const ColorF& color)
	{
		const Circle baseCircle{ center, radius };

		for (int32 i = 0; i < 9; ++i)
		{
			baseCircle.drawPie((-10_deg + i * 40_deg), 20_deg, color, ColorF{ color, 0.0 });
		}
	}
}

void DrawBackground()
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.35 });
			}
		}
	}
}

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

	const Texture texture1{ U"🥗"_emoji };
	const Texture texture2{ U"💰"_emoji };

	while (System::Update())
	{
		DrawBackground();

		{
			const Vec2 pos{ 200, 200 };
			texture1.drawAt(pos);
		}

		{
			const Vec2 pos{ 500, 200 };
			EffectLib::DrawWithOutline(texture1, pos, 3, ColorF{ 0.15 });
		}

		{
			const Vec2 pos{ 800, 200 };
			EffectLib::DrawGlowEffect1(pos, 120, ColorF{ 1.0, 0.9, 0.4 });
			EffectLib::DrawWithOutline(texture1, pos, 3, ColorF{ 0.15 });
		}

		{
			const Vec2 pos{ 1100, 200 };
			EffectLib::DrawGlowEffect1(pos, 120, ColorF{ 1.0, 0.9, 0.4 });
			EffectLib::DrawGlowEffect2(pos, 160, ColorF{ 1.0, 0.9, 0.4 });
			EffectLib::DrawWithOutline(texture1, pos, 3, ColorF{ 0.15 });
		}

		{
			const Vec2 pos{ 200, 500 };
			texture2.drawAt(pos);
		}

		{
			const Vec2 pos{ 500, 500 };
			EffectLib::DrawWithOutline(texture2, pos, 6, ColorF{ 0.96 });
		}

		{
			const Vec2 pos{ 800, 500 };
			EffectLib::DrawGlowEffect1(pos, 120, ColorF{ 0.2, 0.8, 1.0 });
			EffectLib::DrawWithOutline(texture2, pos, 6, ColorF{ 0.96 });
		}

		{
			const Vec2 pos{ 1100, 500 };
			EffectLib::DrawGlowEffect1(pos, 120, ColorF{ 0.2, 0.8, 1.0 });
			EffectLib::DrawGlowEffect2(pos, 160, ColorF{ 0.2, 0.8, 1.0 });
			EffectLib::DrawWithOutline(texture2, pos, 6, ColorF{ 0.96 });
		}
	}
}
Ryo SuzukiRyo Suzuki

テキストを小気味よく登場させる

https://x.com/Reputeless/status/1863954181041868934

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

/// @brief テキストを小気味よく表示します。
/// @param center テキストの中心座標
/// @param font フォント
/// @param text 表示するテキスト
/// @param fontSize フォントサイズ
/// @param textStyle テキストスタイル
/// @param color テキストの色
/// @param t アニメーション進捗(0.0 ~ 1.0)
/// @param step 次の文字が表示されるまでのステップ
/// @param overshootFactor オーバーシュートの強さ(1.0 がデフォルト)
void DrawGlyphsAt(const Vec2& center, const Font& font, const StringView text,
	const double fontSize, const TextStyle& textStyle, const ColorF& color,
	const double t, const double step, const double overshootFactor)
{
	const double scale = (fontSize / font.fontSize());
	const SizeF centerOffset = (font(text).region().size * scale * 0.5);

	const Array<Glyph> glyphs = font.getGlyphs(text);
	const double tt = (t * glyphs.size() * step);
	{
		const ScopedCustomShader2D shader{ Font::GetPixelShader(font.method(), textStyle.type) };
		Graphics2D::SetSDFParameters(textStyle);
		Vec2 penPos{ center - centerOffset };

		for (auto&& [i, glyph] : Indexed(glyphs))
		{
			const double e = Math::Pow(EaseOutBack(Math::Saturate(tt - i * step)), overshootFactor);

			if (e <= 0.0)
			{
				break;
			}

			const Vec2 pos = (penPos + glyph.getOffset(scale)) + (glyph.texture.size * scale * 0.5 * (1.0 - e));
			glyph.texture.scaled(scale * e).draw(pos, color);
			penPos.x += (glyph.xAdvance * scale);
		}
	}
}

void DrawBackground()
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.35 });
			}
		}
	}
}

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

	// フォント
	const Font font = Font{ FontMethod::MSDF, 48, Typeface::Heavy }.setBufferThickness(4);

	// ストップウォッチ
	Stopwatch stopwatch{ StartImmediately::Yes };

	// 表示するテキスト
	TextEditState textEditState{ U"SUCCESS!" };

	// 再生時間
	double duration = 0.6;

	// 次の文字が表示されるまでのステップ
	double step = 0.2;

	// オーバーシュートの強さ
	double overshootFactor = 3.0;

	// 文字の色
	HSV color{ 70.0, 0.9, 1.0 };

	while (System::Update())
	{
		DrawBackground();

		const double t = (stopwatch.sF() / duration);

		DrawGlyphsAt(Scene::Center(), font, textEditState.text, 120,
			TextStyle::OutlineShadow(0.25, ColorF{ 0.11 }, Vec2{ 1.5, 1.5 }, ColorF{ 0.11 }), color, t, step, overshootFactor);

		SimpleGUI::TextBox(textEditState, Vec2{ 60, 520 }, 240);

		if (SimpleGUI::Button(U"\U000F040A", Vec2{ 340, 520 }, 80))
		{
			stopwatch.restart();
		}

		SimpleGUI::Slider(U"Duration: {:.2f}"_fmt(duration), duration, 0.1, 2.0, Vec2{ 60, 560 }, 240);

		SimpleGUI::Slider(U"Step: {:.2f}"_fmt(step), step, 0.01, 1.0, Vec2{ 60, 600 }, 240);

		SimpleGUI::Slider(U"OvershootFactor: {:.1f}"_fmt(overshootFactor), overshootFactor, 1.0, 6.0, Vec2{ 60, 640 }, 240);

		SimpleGUI::ColorPicker(color, Vec2{ 440, 520 });
	}
}
Ryo SuzukiRyo Suzuki

紙吹雪を降らせる

https://x.com/Reputeless/status/1864312596826083508

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

void DrawBackground()
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.86 });
			}
		}
	}
}

/// @brief 紙吹雪クラス
class Confetti
{
public:

	/// @brief 紙吹雪の一片
	struct Piece
	{
		Vec2 startPos{ 0, 0 };

		Vec2 startVelocity{ 0, 0 };

		double startAngle = 0.0;

		double angularVelocity;

		double size = 24.0;

		double hue = 0.0;

		double time = 0.0;

		[[nodiscard]]
		Vec2 getPos(const Vec2& gravity) const noexcept
		{
			return (startPos + startVelocity * time + 0.5 * gravity * time * time);
		}

		[[nodiscard]]
		double getAngle() const noexcept
		{
			return (startAngle + angularVelocity * time);
		}

		[[nodiscard]]
		ColorF getColor() const noexcept
		{
			return HSV{ hue, 0.85, 0.95 };
		}

		void draw(const Vec2& gravity) const
		{
			RectF{ Arg::center(0, 0), size, (size * 0.8) }
				.scaled(Periodic::Sine1_1(3s, time), 1.0)
				.rotated(getAngle()).movedBy(getPos(gravity)).draw(getColor());
		}

		[[nodiscard]]
		static Piece Make(const Vec2& pos, const Vec2& startVelocity, const double size, const double hue) noexcept
		{
			return
			{
				.startPos			= pos,
				.startVelocity		= startVelocity,
				.startAngle			= Random(2_pi),
				.angularVelocity	= (Random(-180_deg, 180_deg) + Random(-180_deg, 180_deg)),
				.size				= size,
				.hue				= hue,
			};
		}
	};

	/// @brief デフォルトコンストラクタ
	[[nodiscard]]
	Confetti() = default;

	/// @brief 紙吹雪を初期化します
	/// @param lifeTime 紙吹雪の寿命
	/// @param gravity 重力
	[[nodiscard]]
	explicit Confetti(const Duration& lifeTime, const Vec2& gravity = Vec2{ 0, 100 })
		: m_lifeTimeSec{ lifeTime.count() }
		, m_gravity{ gravity } {}

	/// @brief 紙吹雪の一片を追加します
	/// @param pos 初期位置
	/// @param startVelocity 初期速度
	/// @param size 紙の大きさ
	/// @param hue 色相
	void add(const Vec2& pos, const Vec2& startVelocity, const double size, const double hue)
	{
		m_pieces << Piece::Make(pos, startVelocity, size, hue);
	}

	/// @brief 紙吹雪をクリアします
	void clear()
	{
		m_pieces.clear();
	}

	/// @brief 紙吹雪を更新します
	/// @param deltaTime 前フレームからの経過時間(秒)
	void update(double deltaTime = Scene::DeltaTime())
	{
		for (auto& piece : m_pieces)
		{
			piece.time += deltaTime;
		}

		m_pieces.remove_if([lifeTime = m_lifeTimeSec](const Piece& piece) { return (lifeTime < piece.time); });
	}

	/// @brief 紙吹雪を描画します
	void draw() const
	{
		for (const auto& piece : m_pieces)
		{
			piece.draw(m_gravity);
		}
	}

	/// @brief 紙吹雪の数を返します
	/// @return 紙吹雪の数
	[[nodiscard]]
	size_t num_pieces() const
	{
		return m_pieces.size();
	}

private:

	Array<Piece> m_pieces;

	double m_lifeTimeSec = 4.0;

	Vec2 m_gravity{ 0, 0 };
};

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.82 });
	const Texture texture{ U"🏆"_emoji };

	Confetti confetti{ 3.5s };
	const double SpawnInterval = 0.03;
	double accumulatedTime = 0.0;

	while (System::Update())
	{
		// 更新
		{
			accumulatedTime += Scene::DeltaTime();

			while (SpawnInterval <= accumulatedTime)
			{
				const Vec2 pos				= RandomVec2(RectF{ -40, -40, (Scene::Width() + 80), 20 });
				const Vec2 startVelocity	= (Vec2{ 0, 240 } + RandomVec2(120.0));
				const double size			= Random(20.0, 28.0);
				const double hue			= Random(360.0);
				confetti.add(pos, startVelocity, size, hue);
				accumulatedTime -= SpawnInterval;
			}

			confetti.update();
		}

		// 描画
		{
			DrawBackground();

			Circle{ Scene::Center(), 100 }.draw(ColorF{ 0.4 });
			texture.drawAt(Scene::Center());

			confetti.draw();
		}
	}
}

Ryo SuzukiRyo Suzuki

図形だけで作る画面遷移アニメーション

https://x.com/Reputeless/status/1864683513871782078

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

void DrawBackground(const Font& font, StringView text)
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.86 });
			}
		}
	}

	font(text).drawAt(TextStyle::OutlineShadow(0.25, ColorF{ 0.11 }, Vec2{ 1.5, 1.5 }, ColorF{ 0.11 }), 180, Scene::Center(), ColorF{ 1.0 });
}

namespace TransitionLib
{
	// 円形の画面が真っ暗に閉じていく
	void IrisOut(double t)
	{
		t = Math::Saturate(t * 1.5);
		const double radius = Scene::Size().length() * 0.5;
		Circle{ Scene::Center(), radius }.drawFrame((t * radius), 0, ColorF{ 0.11 });
	}

	// 真っ暗な画面から円形に開いていく
	void IrisIn(double t)
	{
		t = Math::Saturate(-0.5 + t * 1.5);
		const double radius = Scene::Size().length() * 0.5;
		Circle{ Scene::Center(), radius }.drawFrame(((1.0 - t) * radius), 0, ColorF{ 0.11 });
	}

	// 筆で塗りつぶすように画面が消えていく
	void PaintOut(double t)
	{
		const double f = (t * 6.0);
		const int32 fi = static_cast<int32>(Math::Ceil(f));
		const double rectHeight = (Scene::Height() / 6.0);

		for (int32 y = 0; y < fi; ++y)
		{
			const double s = Math::Saturate(1 - ((y + 1) - f));

			if (IsEven(y))
			{
				RectF{ -100, (rectHeight * y), ((Scene::Width() + 200) * s), rectHeight }.draw(HSV{ 40, s })
					.draw(Arg::left = HSV{ 40 }, Arg::right = HSV{ 40, s });
			}
			else
			{
				RectF{ Arg::topRight((Scene::Width() + 100), (rectHeight * y)), ((Scene::Width() + 200) * s), rectHeight }
					.draw(Arg::left = HSV{ 40, s }, Arg::right = HSV{ 40 });
			}
		}
	}

	// 筆で塗りつぶすように画面が現れる
	void PaintIn(double t)
	{
		const double f = ((1.0 - t) * 6.0);
		const int32 fi = static_cast<int32>(Math::Ceil(f));
		const double rectHeight = (Scene::Height() / 6.0);

		for (int32 y = 0; y < fi; ++y)
		{
			const double s = Math::Saturate(1 - ((y + 1) - f));

			if (IsEven(y))
			{
				RectF{ -100, (rectHeight * (5 - y)), ((Scene::Width() + 200) * s), rectHeight }
					.draw(Arg::left = HSV{ 40 }, Arg::right = HSV{ 40, s });
			}
			else
			{
				RectF{ Arg::topRight((Scene::Width() + 100), (rectHeight * (5 - y))), ((Scene::Width() + 200) * s), rectHeight }
					.draw(Arg::left = HSV{ 40, s }, Arg::right = HSV{ 40 });
			}
		}
	}

	// 回転する四角形で画面が消えていく
	void QuadOut(double t)
	{
		t = EaseInOutExpo(t);
		Polygon polygon = Scene::Rect().scaledAt(Scene::Center(), 4).asPolygon();
		const Quad hole = Scene::Rect().scaledAt(Scene::Center(), (1.0 - t)).rotatedAt(Scene::Center(), (t * 360_deg));

		if (polygon.addHole(hole))
		{
			polygon.draw(HSV{ 200, 0.5, 1.0 });
		}
		else
		{
			Scene::Rect().draw(HSV{ 200, 0.5, 1.0 });
		}
	}

	// 回転する四角形で画面が現れる
	void QuadIn(double t)
	{
		t = EaseInOutExpo(t);
		Polygon polygon = Scene::Rect().scaledAt(Scene::Center(), 4).asPolygon();
		const Quad hole = Scene::Rect().scaledAt(Scene::Center(), t).rotatedAt(Scene::Center(), (t * 360_deg));

		if (polygon.addHole(hole))
		{
			polygon.draw(HSV{ 200, 0.5, 1.0 });
		}
		else
		{
			Scene::Rect().draw(HSV{ 200, 0.5, 1.0 });
		}
	}
}

struct GameData
{
	static constexpr Duration TransitionTime = 1.5s;

	Font font = Font{ FontMethod::MSDF, 48, Typeface::Heavy }.setBufferThickness(4);
};

using App = SceneManager<int32, GameData>;

class Scene0 : public App::Scene
{
public:

	Scene0(const InitData& init)
		: IScene{ init } {}

	void update() override
	{
		if (MouseL.down())
		{
			changeScene(1, GameData::TransitionTime);
		}
	}

	void draw() const override
	{
		DrawBackground(getData().font, U"Scene: {}"_fmt(getState()));
	}

	void drawFadeIn(double t) const override
	{
		draw();
		TransitionLib::IrisIn(t);
	}

	void drawFadeOut(double t) const override
	{
		draw();
		TransitionLib::PaintOut(t);
	}
};

class Scene1 : public App::Scene
{
public:

	Scene1(const InitData& init)
		: IScene{ init } {}

	void update() override
	{
		if (MouseL.down())
		{
			changeScene(2, GameData::TransitionTime);
		}
	}

	void draw() const override
	{
		DrawBackground(getData().font, U"Scene: {}"_fmt(getState()));
	}

	void drawFadeIn(double t) const override
	{
		draw();
		TransitionLib::PaintIn(t);
	}

	void drawFadeOut(double t) const override
	{
		draw();
		TransitionLib::QuadOut(t);
	}
};

class Scene2 : public App::Scene
{
public:

	Scene2(const InitData& init)
		: IScene{ init } {}

	void update() override
	{
		if (MouseL.down())
		{
			changeScene(0, GameData::TransitionTime);
		}
	}

	void draw() const override
	{
		DrawBackground(getData().font, U"Scene: {}"_fmt(getState()));
	}

	void drawFadeIn(double t) const override
	{
		draw();
		TransitionLib::QuadIn(t);
	}

	void drawFadeOut(double t) const override
	{
		draw();
		TransitionLib::IrisOut(t);
	}
};

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

	App manager;
	manager.add<Scene0>(0);
	manager.add<Scene1>(1);
	manager.add<Scene2>(2);
	manager.init(0, GameData::TransitionTime);

	while (System::Update())
	{
		if (not manager.update())
		{
			break;
		}
	}
}
Ryo SuzukiRyo Suzuki

ノードグラフ

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

void DrawBackground()
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.24 });
			}
		}
	}
}

struct Node
{
	using IDType = uint32;

	IDType nodeID = 0;

	Rect rect{ 0 };

	uint64 lastUpdateTime = 0;

	void draw() const
	{
		rect.rounded(8)
			.drawShadow(Vec2{ 0, 3 }, 12, 3, ColorF{ 0.0, 0.5 }, false)
			.draw(ColorF{ 0.04 });

		const RectF titleRect{ rect.pos, rect.w, 36 };
		titleRect.rounded(8, 8, 0, 0).draw(ColorF{ 0.1, 0.5, 0.2 });
		SimpleGUI::GetFont()(U"Node #{}"_fmt(nodeID))
			.drawAt(TextStyle::Shadow(Vec2{ 1, 1 }, ColorF{ 0.11 }), 22, titleRect.center().movedBy(0, -1), ColorF{ 0.96 });
	}

	[[nodiscard]]
	Vec2 getInputPos() const
	{
		return rect.leftCenter().movedBy(20, 0);
	}

	[[nodiscard]]
	Vec2 getOutputPos() const
	{
		return rect.rightCenter().movedBy(-20, 0);
	}
};

struct Wire
{
	Node::IDType from;

	Node::IDType to;
};

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

	HashTable<Node::IDType, Node> nodes;
	nodes.emplace(1, Node{ 1, Rect{ 100, 300, 200, 300 } });
	nodes.emplace(2, Node{ 2, Rect{ 500, 100, 200, 300 } });
	nodes.emplace(3, Node{ 3, Rect{ 900, 400, 200, 300 } });

	Array<Wire> wires;
	wires << Wire{ 1, 2 };
	wires << Wire{ 2, 3 };

	// ドラッグ中のノードの ID
	Optional<Node::IDType> draggingNodeID;

	while (System::Update())
	{
		// 更新
		{
			if (draggingNodeID && MouseL.up())
			{
				draggingNodeID.reset();
			}

			for (auto&& [nodeID, node] : nodes)
			{
				if (node.rect.leftClicked())
				{
					draggingNodeID = nodeID;
					node.lastUpdateTime = Time::GetMillisecSinceEpoch();
				}
				else if (draggingNodeID == nodeID)
				{
					node.rect.moveBy(Cursor::Delta());
				}
			}
		}

		// 描画
		{
			DrawBackground();

			// 更新時間の昇順に描画する
			{
				Array<std::pair<uint64, Node::IDType>> sortedNodes;

				for (auto&& [nodeID, node] : nodes)
				{
					sortedNodes.emplace_back(node.lastUpdateTime, nodeID);
				}

				sortedNodes.sort();

				for (const auto& [_unused, nodeID] : sortedNodes)
				{
					nodes[nodeID].draw();
				}
			}

			for (const auto& wire : wires)
			{
				const Vec2 p0 = nodes[wire.from].getOutputPos();
				const Vec2 p3 = nodes[wire.to].getInputPos();
				const Vec2 pCenter = p0.getMidpoint(p3);
				const Vec2 p1{ pCenter.x, p0.y };
				const Vec2 p2{ pCenter.x, p3.y };
				const Bezier3 bezier{ p0, p1, p2, p3 };

				bezier.draw(5, ColorF{ 0.7 });
				p0.asCircle(8).draw(ColorF{ 0.7 });
				p3.asCircle(8).draw(ColorF{ 0.7 });
			}
		}
	}
}
Ryo SuzukiRyo Suzuki

計算機能付きテキストボックス

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

void DrawBackground()
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.25 });
			}
		}
	}
}

class CalcTextBox
{
public:

	[[nodiscard]]
	CalcTextBox() = default;

	[[nodiscard]]
	explicit CalcTextBox(double defaultValue)
		: m_textEditState{ Format(defaultValue) }
		, m_value{ defaultValue } {}

	void update(const Vec2& pos, double width)
	{
		if (SimpleGUI::TextBox(m_textEditState, pos, width))
		{
			m_active = true;
		}

		if (m_active && (not m_textEditState.active))
		{
			m_active = false;

			if (const auto value = EvalOpt(m_textEditState.text))
			{
				m_value = *value;
			}

			m_textEditState = TextEditState{ Format(m_value) };
		}
	}

	[[nodiscard]]
	double value() const
	{
		return m_value;
	}

private:

	TextEditState m_textEditState;

	double m_value = 0.0;

	bool m_active = false;
};

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

	CalcTextBox c0{ 400.0 };
	CalcTextBox c1{ 200.0 };

	while (System::Update())
	{
		DrawBackground();

		Rect{ 80, 80, 240, 160 }.rounded(20).draw(ColorF{ 0.9 });

		SimpleGUI::GetFont()(U"W").draw(21, Vec2{ 100, 120 }, ColorF{ 0.11 });
		c0.update(Vec2{ 130, 120 }, 160);

		SimpleGUI::GetFont()(U"H").draw(21, Vec2{ 100, 160 }, ColorF{ 0.11 });
		c1.update(Vec2{ 130, 160 }, 160);

		RectF{ 400, 80, c0.value(), c1.value() }
			.drawShadow(Vec2{ 2, 2 }, 12, 1)
			.draw(ColorF{ 0.2, 0.5, 0.8 });
	}
}
Ryo SuzukiRyo Suzuki

テクスチャ付きテキストを描く

  • 文字部分だけ非透過にしたレンダーテクスチャに、自由に描画します。

https://x.com/Reputeless/status/1865771381365039460

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

void DrawBackground()
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.28 });
			}
		}
	}
}

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

	const MSRenderTexture renderTexture{ Size{ 1280, 720 } };
	const Font font{ FontMethod::MSDF, 64, Typeface::Heavy };

	// 泡
	Array<Circle> bubbles;
	for (int32 i = 0; i < 80; ++i)
	{
		bubbles.push_back(Circle{ RandomVec2(RectF{ renderTexture.size() }), Random(6.0, 12.0) });
	}

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

		// 泡を移動させる
		for (auto& bubble : bubbles)
		{
			bubble.moveBy(0, bubble.r * (-24 * deltaTime));

			if ((bubble.y + bubble.r) < 0)
			{
				bubble.r = Random(6.0, 12.0);
				bubble.x = Random(renderTexture.width());
				bubble.y += (renderTexture.height() + bubble.r);
			}
		}

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

				// テキストをアルファだけ描く
				{
					BlendState bs = BlendState::Default2D;
					bs.srcAlpha = Blend::One;
					bs.dstAlpha = Blend::One;
					bs.opAlpha = BlendOp::Max;
					const ScopedRenderStates2D state{ bs };
					font(U"Siv3D").drawAt(240, (renderTexture.size() / 2).movedBy(0, -140));
					font(U"文字の描画").drawAt(240, (renderTexture.size() / 2).movedBy(0, 140));
				}

				// 背景のグラデーションを描く
				Rect{ renderTexture.size() }.draw(Arg::top(0.6, 1.0, 1.0), Arg::bottom(0.0, 0.3, 0.7));

				// 泡を描く
				for (const auto& bubble : bubbles)
				{
					bubble.draw(ColorF{ 1.0, 0.3 });
				}
			}

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

		DrawBackground();

		// テクスチャ付きテキストを描く
		renderTexture.draw();

		// テキストの輪郭を描く
		font(U"Siv3D").drawAt(TextStyle::Outline(0.1, ColorF{ 0.11 }), 240, (renderTexture.size() / 2).movedBy(0, -140), ColorF{ 0.0, 0.0 });
		font(U"文字の描画").drawAt(TextStyle::Outline(0.1, ColorF{ 0.11 }), 240, (renderTexture.size() / 2).movedBy(0, 140), ColorF{ 0.0, 0.0 });
	}
}
Ryo SuzukiRyo Suzuki

画像を Polygon で切り抜く

https://x.com/Reputeless/status/1866121803107856780

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

void DrawBackground()
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.88 });
			}
		}
	}
}

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

	LineString points;
	Polygon polygon, polygonBuffered;
	Buffer2D buffer2D;

	while (System::Update())
	{
		if (MouseL.pressed())
		{
			const Point pos = Cursor::Pos();

			if (points.isEmpty() || points.back() != pos)
			{
				points << pos;
			}
		}

		if (MouseL.up())
		{
			if (points.size() <= 3)
			{
				polygon = Polygon{};
			}
			else
			{
				polygon = Polygon::CorrectOne(points).simplified(1);
				polygonBuffered = polygon.calculateBuffer(2);
				buffer2D = polygon.toBuffer2D(Vec2{ 0, 0 }, texture.size());
			}

			points.clear();
		}

		DrawBackground();
		texture.region().draw();
		texture.draw();
		points.draw(4.0, ColorF{ 1.0, 0.0, 0.0, 0.5 });

		if (polygon)
		{
			polygon.draw(ColorF{ 1.0, 0.0, 0.0, 0.5 });

			{
				const Vec2 center{ 800, 360 };
				const Vec2 offset = (center - polygon.boundingRect().pos - (polygon.boundingRect().size / 2));
				const Transformer2D tr{ Mat3x2::Translate(offset).scaled(2, center) };

				for (int32 y = 0; y < 3; ++y)
				{
					const Transformer2D tr2{ Mat3x2::Translate(2, y) };
					polygonBuffered.draw(ColorF{ 0.7 });
				}

				polygon.draw(ColorF{ 0.6, 0.9, 1.0 });
				polygon.drawFrame(3.0, ColorF{ 0.99 });
				buffer2D.draw(texture);
			}
		}
	}
}
Ryo SuzukiRyo Suzuki

炎で侵食するシェーダ

  • 炎による侵食の効果を伴ってテクスチャを描くシェーダです。

https://x.com/Reputeless/status/1866468035701408020

fire_dissolve.hlsl
//
//	Textures
//
Texture2D		g_texture0 : register(t0);
Texture2D		g_texture1 : register(t1);
SamplerState	g_sampler0 : register(s0);
SamplerState	g_sampler1 : register(s1);

namespace s3d
{
	//
	//	VS Output / PS Input
	//
	struct PSInput
	{
		float4 position	: SV_POSITION;
		float4 color	: COLOR0;
		float2 uv		: TEXCOORD0;
	};
}

//
//	Constant Buffer
//
cbuffer PSConstants2D : register(b0)
{
	float4 g_colorAdd;
	float4 g_sdfParam;
	float4 g_sdfOutlineColor;
	float4 g_sdfShadowColor;
	float4 g_internal;
}

cbuffer FireDissolve : register(b1)
{
	float g_dissolveThreshold;
	float g_flameThreshold;
	// float2 _padding;
	float3 g_flameColor;
}

float4 PS(s3d::PSInput input) : SV_TARGET
{
	float4 texColor = g_texture0.Sample(g_sampler0, input.uv);
	
	float value = g_texture1.Sample(g_sampler1, input.uv).r;
	float diff = (value - g_dissolveThreshold);
	
	float burn = smoothstep(g_flameThreshold, 0.0, diff);
	texColor.rgb += (burn * g_flameColor);

	float dissolve = smoothstep(0.0, 0.02, diff);
	texColor.a *= dissolve;
	
	return (texColor * input.color) + g_colorAdd;
}
fire_dissolve.frag
# version 410

//
//	Textures
//
uniform sampler2D Texture0;
uniform sampler2D Texture1;

//
//	PSInput
//
layout(location = 0) in vec4 Color;
layout(location = 1) in vec2 UV;

//
//	PSOutput
//
layout(location = 0) out vec4 FragColor;

//
//	Constant Buffer
//
layout(std140) uniform PSConstants2D
{
	vec4 g_colorAdd;
	vec4 g_sdfParam;
	vec4 g_sdfOutlineColor;
	vec4 g_sdfShadowColor;
	vec4 g_internal;
};

layout(std140) uniform FireDissolve
{
	float g_dissolveThreshold;
	float g_flameThreshold;
	// vec2 _padding;
	vec3 g_flameColor;
};

//
//	Functions
//
void main()
{
	vec4 texColor = texture(Texture0, UV);
	
	float value = texture(Texture1, UV).r;
	float diff = (value - g_dissolveThreshold);

	float burn = smoothstep(g_flameThreshold, 0.0, diff);
	texColor.rgb += (burn * g_flameColor);

	float dissolve = smoothstep(0.0, 0.02, diff);
	texColor.a *= dissolve;

	FragColor = (texColor * Color) + g_colorAdd;
}
# include <Siv3D.hpp> // Siv3D v0.6.15

void DrawBackground()
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.25 });
			}
		}
	}
}

/// @brief 指定されたサイズのノイズ画像を生成します。
/// @param size 画像のサイズ
/// @return 生成したノイズ画像
Image GenerateNoiseImage(const Size& size)
{
	Image image{ size };

	PerlinNoise noise{ RandomUint32() };

	for (int32 y = 0; y < size.y; ++y)
	{
		for (int32 x = 0; x < size.x; ++x)
		{
			const double t = noise.octave2D0_1((x / 64.0), (y / 64.0), 5);
			image[y][x] = ColorF{ t };
		}
	}

	return image;
}

/// @brief 炎による侵食関連の定数バッファ
struct FireDissolve
{
	float dissolveThreshold;
	float flameThreshold;
	Float2 _padding;
	Float3 flameColor;
};

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

	const Texture texture{ U"example/siv3d-kun.png", TextureDesc::Mipped };
	Texture noiseTexture{ GenerateNoiseImage(texture.size()), TextureDesc::Mipped };

	const PixelShader ps = HLSL{ U"fire_dissolve.hlsl", U"PS" } | GLSL{ U"fire_dissolve.frag", {{ U"PSConstants2D", 0 }, { U"FireDissolve", 1 }} };
	if (not ps)
	{
		throw Error{ U"Failed to load a shader file" };
	}
	ConstantBuffer<FireDissolve> cb;

	// 侵食の閾値
	double dissolveThreshold = 0.0;

	// 炎の閾値
	double flameThreshold = 0.1;

	// 炎の色
	HSV flameColor{ 30, 1.0, 1.0 };

	while (System::Update())
	{
		cb->dissolveThreshold = static_cast<float>(dissolveThreshold);
		cb->flameThreshold = static_cast<float>(flameThreshold);
		cb->flameColor = flameColor.toColorF().rgb();

		// 背景と比較用のテクスチャ描画
		{
			DrawBackground();
			texture.drawAt(Scene::Rect().getRelativePoint(0.2, 0.4));
			noiseTexture.drawAt(Scene::Rect().getRelativePoint(0.8, 0.4));
		}

		// 炎による侵食を伴うテクスチャ描画
		{
			Graphics2D::SetPSConstantBuffer(1, cb);
			Graphics2D::SetPSTexture(1, noiseTexture);
			const ScopedCustomShader2D shader{ ps };
			texture.drawAt(Scene::Rect().getRelativePoint(0.5, 0.4));
		}

		// GUI
		{
			SimpleGUI::Slider(U"dissolveThreshold: {:.2f}"_fmt(dissolveThreshold), dissolveThreshold, -0.2, 1.0, Vec2{ 120, 560 }, 240, 240);
			SimpleGUI::Slider(U"flameThreshold: {:.2f}"_fmt(flameThreshold), flameThreshold, 0.0, 0.2, Vec2{ 120, 600 }, 240, 240);
			SimpleGUI::ColorPicker(flameColor, Vec2{ 640, 560 });
			if (SimpleGUI::Button(U"Regenerate", Vec2{ 840, 560 }))
			{
				noiseTexture = Texture{ GenerateNoiseImage(texture.size()), TextureDesc::Mipped };
			}
		}
	}
}
Ryo SuzukiRyo Suzuki

頂点シェーダで波を描く

https://x.com/Reputeless/status/1866859243556704699

wave.hlsl
namespace s3d
{
	//
	//	VS Output
	//
	struct PSInput
	{
		float4 position	: SV_POSITION;
		float4 color	: COLOR0;
		float2 uv		: TEXCOORD0;
	};

	//
	//	Siv3D Functions
	//
	float4 Transform2D(float2 pos, float2x4 t)
	{
		return float4((t._13_14 + (pos.x * t._11_12) + (pos.y * t._21_22)), t._23_24);
	}
}

//
//	Constant Buffer
//
cbuffer VSConstants2D : register(b0)
{
	row_major float2x4 g_transform;
	float4 g_colorMul;
}

cbuffer SoftShape : register(b1)
{
	float g_t;
}

//
//	Functions
//
float CalcWave(float x, float t)
{
    return sin(x * 0.011 + t) * 6
		+ sin(x * 0.013 + -t * 1.1) * 4
		+ sin(x * 0.017 + t * 1.3) * 3;
}

s3d::PSInput VS(uint id: SV_VERTEXID)
{
	s3d::PSInput result;

	float2 pos;
    float4 color;
	
    uint xi = (id / 6);
    uint i = (id % 6);
	
    if ((i == 0) || (i == 2) || (i == 3))
    {
        pos.x = (xi * 8);
    }
	else
    {
        pos.x = ((xi + 1) * 8);
    }
	
    if ((i == 2) || (i == 3) || (i == 5))
    {
        pos.y = 720;
        color = float4(0.0, 0.3, 0.6, 1.0);
    }
	else
    {
        pos.y = 480 + CalcWave(pos.x, g_t);
        color = float4(0.1, 0.6, 0.9, 1.0);
    }

    result.position = s3d::Transform2D(pos, g_transform);
    result.color = color;
    result.uv = float2(0, 0);
    return result;
}
wave.vert
# version 410

//
//	VS Input (nothing)
//

//
//	VS Output
//
layout(location = 0) out vec4 Color;
layout(location = 1) out vec2 UV;
out gl_PerVertex
{
	vec4 gl_Position;
};

//
//	Siv3D Functions
//
vec4 s3d_Transform2D(const vec2 pos, const vec4 t[2])
{
	return vec4(t[0].zw + (pos.x * t[0].xy) + (pos.y * t[1].xy), t[1].zw);
}

//
//	Constant Buffer
//
layout(std140) uniform VSConstants2D
{
	vec4 g_transform[2];
	vec4 g_colorMul;
};

layout(std140) uniform Wave
{
	float g_t;
};

//
//	Functions
//
float CalcWave(float x, float t)
{
    return sin(x * 0.011 + t) * 6
		+ sin(x * 0.013 + -t * 1.1) * 4
		+ sin(x * 0.017 + t * 1.3) * 3;
}

void main()
{
	vec2 pos;
	vec4 color;

	uint xi = (gl_VertexID / 6);
	uint i = (gl_VertexID % 6);

	if ((i == 0) || (i == 2) || (i == 3))
	{
		pos.x = (xi * 8);
	}
	else
	{
		pos.x = ((xi + 1) * 8);
	}

    if ((i == 2) || (i == 3) || (i == 5))
    {
        pos.y = 720;
        color = vec4(0.0, 0.3, 0.6, 1.0);
    }
	else
    {
        pos.y = 480 + CalcWave(pos.x, g_t);
        color = vec4(0.1, 0.6, 0.9, 1.0);
    }

	gl_Position = s3d_Transform2D(pos, g_transform);	
	Color = color;
	UV = vec2(0, 0);
}
# include <Siv3D.hpp>

void DrawBackground()
{
	for (int32 y = 0; y < (Scene::Height() / 40); ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / 40); ++x)
		{
			if (IsEven(x + y))
			{
				RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.85 });
			}
		}
	}
}

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

	const VertexShader vs
		= HLSL{ U"wave.hlsl" }
		| GLSL{ U"wave.vert", { { U"VSConstants2D", 0 }, { U"Wave", 1 } } };

	if (not vs)
	{
		throw Error{ U"Failed to load a shader file" };
	}

	ConstantBuffer<float> cb;

	bool showWireframe = false;

	while (System::Update())
	{
		cb = static_cast<float>(Scene::Time());

		DrawBackground();
		{
			Graphics2D::SetVSConstantBuffer(1, cb);
			const ScopedRenderStates2D states{ showWireframe ? RasterizerState::WireframeCullNone : RasterizerState::Default2D };
			const ScopedCustomShader2D shader{ vs };

			// 頂点情報の無い三角形を 2 * 160 個描画する
			// (頂点情報は頂点シェーダで設定する)
			Graphics2D::DrawTriangles(2 * 160);
		}

		SimpleGUI::CheckBox(showWireframe, U"Wireframe", Vec2{ 60, 60 });
	}
}