💯

Siv3D | テキスト描画テクニック集

2020/09/27に公開

1. グラデーションのあるテキストを描く

# include <Siv3D.hpp> // OpenSiv3D v0.6.2

void DrawGradientText(const Font& font, const String& text, const Vec2& pos, const ColorF& topColor, const ColorF& bottomColor)
{
	const Vec2 basePos = pos;

	Vec2 penPos{ basePos };

	// 文字単位で描画を制御するためのループ
	for (const auto& glyph : font.getGlyphs(text))
	{
		// 改行文字なら
		if (glyph.codePoint == U'\n')
		{
			// ペンの X 座標をリセット
			penPos.x = basePos.x;

			// ペンの Y 座標をフォントの高さ分進める
			penPos.y += font.height();

			continue;
		}

		const Vec2 offset = glyph.getOffset();
		const double topPos = offset.y;
		const double bottomPos = (offset.y + glyph.texture.size.y);

		const double topT = (topPos / font.height());
		const double bottomT = (bottomPos / font.height());

		// グラデーションの色
		const ColorF c1 = topColor.lerp(bottomColor, topT);
		const ColorF c2 = topColor.lerp(bottomColor, bottomT);

		// 文字のテクスチャを描画
		glyph.texture
			.draw(penPos + offset, Arg::top = c1, Arg::bottom = c2);

		penPos.x += glyph.xAdvance;
	}
}

void Main()
{
	const Font font{ 60, Typeface::Heavy };

	while (System::Update())
	{
		const String text = U"OpenSiv3D\nABCDEFG\n1234567\nあいうえお\n{}"_fmt(Cursor::Pos());

		DrawGradientText(font, text,
			Vec2{ 40, 40 }, HSV{ 180, 0.6, 1 }, HSV{ 240, 0.8, 0.8 });
	}
}
v0.4.3
# include <Siv3D.hpp> // OpenSiv3D v0.4.3

void DrawGradientText(const Font& font, const String& text, const Vec2& pos, const ColorF& topColor, const ColorF& bottomColor)
{
	const Vec2 basePos = pos;

	Vec2 penPos(basePos);

	// 文字単位で描画を制御するためのループ
	for (const auto& glyph : font(text))
	{
		// 改行文字なら
		if (glyph.codePoint == U'\n')
		{
			// ペンの X 座標をリセット
			penPos.x = basePos.x;

			// ペンの Y 座標をフォントの高さ分進める
			penPos.y += font.height();

			continue;
		}

		const double topPos = glyph.offset.y;
		const double bottomPos = (glyph.offset.y + glyph.texture.size.y);
		
		const double topT = (topPos / font.height());
		const double bottomT = (bottomPos / font.height());

		// グラデーションの色
		const ColorF c1 = topColor.lerp(bottomColor, topT);
		const ColorF c2 = topColor.lerp(bottomColor, bottomT);

		// 文字のテクスチャを描画
		glyph.texture
			.draw(penPos + glyph.offset, Arg::top = c1, Arg::bottom = c2);

		penPos.x += glyph.xAdvance;
	}
}

void Main()
{
	const Font font(60, Typeface::Heavy);

	while (System::Update())
	{
		const String text = U"OpenSiv3D\nABCDEFG\n1234567\nあいうえお\n{}"_fmt(Cursor::Pos());

		DrawGradientText(font, text,
			Vec2(40, 40), HSV(180, 0.6, 1), HSV(240, 0.8, 0.8));
	}
}

2. 光彩のあるテキストを描く

# include <Siv3D.hpp> // OpenSiv3D v0.6.2

// 光彩用の RenderTexture 管理クラス
class GlowText
{
private:

	Size m_basiSize{ 0, 0 };
	mutable RenderTexture m_gaussianA1, m_gaussianB1;
	mutable RenderTexture m_gaussianA4, m_gaussianB4;
	mutable RenderTexture m_gaussianA8, m_gaussianB8;

public:

	GlowText() = default;

	GlowText(int32 width, int32 height)
		: GlowText{ Size{ width, height } } {}

	explicit GlowText(const Size& size)
		: m_basiSize{ size }
		, m_gaussianA1{ size }, m_gaussianB1{ size }
		, m_gaussianA4{ size / 4 }, m_gaussianB4{ size / 4 }
		, m_gaussianA8{ size / 8 }, m_gaussianB8{ size / 8 } {}

	void renderGlow(const Font& font, const String& text, const Vec2& pos) const
	{
		{
			const ScopedRenderTarget2D target(m_gaussianA1.clear(ColorF{ 0.0 }));
			font(text).draw(pos);
		}
		// オリジナルサイズのガウスぼかし (A1)
		// A1 を 1/4 サイズにしてガウスぼかし (A4)
		// A4 を 1/2 サイズにしてガウスぼかし (A8)
		Shader::GaussianBlur(m_gaussianA1, m_gaussianB1, m_gaussianA1);
		Shader::Downsample(m_gaussianA1, m_gaussianA4);
		Shader::GaussianBlur(m_gaussianA4, m_gaussianB4, m_gaussianA4);
		Shader::Downsample(m_gaussianA4, m_gaussianA8);
		Shader::GaussianBlur(m_gaussianA8, m_gaussianB8, m_gaussianA8);
	}

	void draw(const Vec2& pos, const ColorF& glowColor, double a1, double a4, double a8) const
	{
		const ScopedRenderStates2D blend(BlendState::Additive);

		if (a1)
		{
			m_gaussianA1.resized(m_basiSize)
				.draw(pos, ColorF{ glowColor, a1 });
		}

		if (a4)
		{
			m_gaussianA4.resized(m_basiSize)
				.draw(pos, ColorF{ glowColor, a4 });
		}

		if (a8)
		{
			m_gaussianA8.resized(m_basiSize)
				.draw(pos, ColorF{ glowColor, a8 });
		}
	}
};

void Main()
{
	const Font font{ 60, Typeface::Heavy };

	GlowText glowText{ 800, 600 }; // 必要最小限のサイズにすると実行時性能向上
	double a8 = 0.0, a4 = 0.0, a1 = 0.0;
	HSV glowColor{ 120 };
	HSV textColor = Palette::White;

	while (System::Update())
	{
		const String text = U"OpenSiv3D\nABCDEFG\n1234567\nあいうえお\n{}"_fmt(Cursor::Pos());
		constexpr Vec2 pos{ 280, 40 };

		// 光彩を作成
		// 前フレームから内容を更新しない場合、スキップすることで実行時のコストを節約できる
		glowText.renderGlow(font, text, pos);

		// 光彩を描画
		glowText.draw(Vec2{ 0, 0 }, glowColor, a1, a4, a8);

		// テキストを描画
		font(text).draw(pos, textColor);

		// 光彩の強さや色を調整
		SimpleGUI::Slider(U"a8: {:.1f}"_fmt(a8), a8, 0.0, 4.0, Vec2{ 20, 20 });
		SimpleGUI::Slider(U"a4: {:.1f}"_fmt(a4), a4, 0.0, 4.0, Vec2{ 20, 60 });
		SimpleGUI::Slider(U"a1: {:.1f}"_fmt(a1), a1, 0.0, 4.0, Vec2{ 20, 100 });
		SimpleGUI::ColorPicker(glowColor, Vec2{ 20, 140 });
		SimpleGUI::ColorPicker(textColor, Vec2{ 20, 260 });
	}
}
v0.4.3
# include <Siv3D.hpp> // OpenSiv3D v0.4.3

// 光彩用の RenderTexture 管理クラス
class GlowText
{
private:

	Size m_basiSize = Size(0, 0);
	mutable RenderTexture m_gaussianA1, m_gaussianB1;
	mutable RenderTexture m_gaussianA4, m_gaussianB4;
	mutable RenderTexture m_gaussianA8, m_gaussianB8;

public:

	GlowText() = default;

	GlowText(int32 width, int32 height)
		: GlowText(Size(width, height)) {}

	explicit GlowText(const Size& size)
		: m_basiSize(size)
		, m_gaussianA1(size), m_gaussianB1(size)
		, m_gaussianA4(size / 4), m_gaussianB4(size / 4)
		, m_gaussianA8(size / 8), m_gaussianB8(size / 8) {}

	void renderGlow(const Font& font, const String& text, const Vec2& pos) const
	{
		m_gaussianA1.clear(ColorF(0.0));
		{
			ScopedRenderTarget2D rt(m_gaussianA1);
			font(text).draw(pos);
		}
		// オリジナルサイズのガウスぼかし (A1)
		// A1 を 1/4 サイズにしてガウスぼかし (A4)
		// A4 を 1/2 サイズにしてガウスぼかし (A8)
		Shader::GaussianBlur(m_gaussianA1, m_gaussianB1, m_gaussianA1);
		Shader::Downsample(m_gaussianA1, m_gaussianA4);
		Shader::GaussianBlur(m_gaussianA4, m_gaussianB4, m_gaussianA4);
		Shader::Downsample(m_gaussianA4, m_gaussianA8);
		Shader::GaussianBlur(m_gaussianA8, m_gaussianB8, m_gaussianA8);
	}

	void draw(const Vec2& pos, const ColorF& glowColor, double a1, double a4, double a8) const
	{
		ScopedRenderStates2D belnd(BlendState::Additive);

		if (a1)
		{
			m_gaussianA1.resized(m_basiSize)
				.draw(pos, ColorF(glowColor, a1));
		}

		if (a4)
		{
			m_gaussianA4.resized(m_basiSize)
				.draw(pos, ColorF(glowColor, a4));
		}

		if (a8)
		{
			m_gaussianA8.resized(m_basiSize)
				.draw(pos, ColorF(glowColor, a8));
		}
	}
};

void Main()
{
	const Font font(60, Typeface::Heavy);

	GlowText glowText(800, 600); // 必要最小限のサイズにすると実行時性能向上
	double a8 = 0.0, a4 = 0.0, a1 = 0.0;
	HSV glowColor = HSV(120);
	HSV textColor = Palette::White;

	while (System::Update())
	{
		const String text = U"OpenSiv3D\nABCDEFG\n1234567\nあいうえお\n{}"_fmt(Cursor::Pos());
		constexpr Vec2 pos(280, 40);

		// 光彩を作成
		// 前フレームから内容を更新しない場合、スキップすることで実行時のコストを節約できる
		glowText.renderGlow(font, text, pos);

		// 光彩を描画
		glowText.draw(Vec2(0, 0), glowColor, a1, a4, a8);

		// テキストを描画
		font(text).draw(pos, textColor);

		// 光彩の強さや色を調整
		SimpleGUI::Slider(U"a8: {:.1f}"_fmt(a8), a8, 0.0, 4.0, Vec2(20, 20));
		SimpleGUI::Slider(U"a4: {:.1f}"_fmt(a4), a4, 0.0, 4.0, Vec2(20, 60));
		SimpleGUI::Slider(U"a1: {:.1f}"_fmt(a1), a1, 0.0, 4.0, Vec2(20, 100));
		SimpleGUI::ColorPicker(glowColor, Vec2(20, 140));
		SimpleGUI::ColorPicker(textColor, Vec2(20, 260));
	}
}

3. テキストの反射

# include <Siv3D.hpp> // OpenSiv3D v0.6.2

void DrawTextWithReflection(const Font& font, const String& text, const Vec2& pos, double offsetY, const ColorF& color)
{
	const Vec2 basePos = pos;

	Vec2 penPos{ basePos };

	// 文字単位で描画を制御するためのループ
	for (const auto& glyph : font.getGlyphs(text))
	{
		// 改行文字なら
		if (glyph.codePoint == U'\n')
		{
			// ペンの X 座標をリセット
			penPos.x = basePos.x;

			// ペンの Y 座標をフォントの高さ分進める
			penPos.y += font.height();

			continue;
		}

		const Vec2 offset = glyph.getOffset();

		// 文字のテクスチャをペンの位置に文字ごとのオフセットを加算して描画
		glyph.texture.draw(penPos + offset, color);

		// 反射するテクスチャ
		glyph.texture.flipped()
			.draw(penPos.x + offset.x, penPos.y + (font.height() * 2) - offset.y - glyph.texture.size.y + offsetY,
				Arg::top = ColorF{ color, 0.5 }, Arg::bottom = ColorF{ color, 0.0 });

		// ペンの X 座標を文字の幅の分進める
		penPos.x += glyph.xAdvance;
	}
}

void Main()
{
	const Font font{ 50 };

	const String text = U"OpenSiv3D あいうえお 12345";

	while (System::Update())
	{
		DrawTextWithReflection(font, text, Vec2{ 40, 40 }, -5, HSV{ 40 });
	}
}
v0.4.3
# include <Siv3D.hpp> // OpenSiv3D v0.4.3

void DrawTextWithReflection(const Font& font, const String& text, const Vec2& pos, double offsetY, const ColorF& color)
{
	const Vec2 basePos = pos;

	Vec2 penPos(basePos);

	// 文字単位で描画を制御するためのループ
	for (const auto& glyph : font(text))
	{
		// 改行文字なら
		if (glyph.codePoint == U'\n')
		{
			// ペンの X 座標をリセット
			penPos.x = basePos.x;

			// ペンの Y 座標をフォントの高さ分進める
			penPos.y += font.height();

			continue;
		}

		// 文字のテクスチャをペンの位置に文字ごとのオフセットを加算して描画
		glyph.texture.draw(penPos + glyph.offset, color);

		// 反射するテクスチャ
		glyph.texture.flipped()
			.draw(penPos.x + glyph.offset.x, penPos.y + (font.height() * 2) - glyph.offset.y - glyph.texture.size.y + offsetY,
				Arg::top = ColorF(color, 0.5), Arg::bottom = ColorF(color, 0.0));

		// ペンの X 座標を文字の幅の分進める
		penPos.x += glyph.xAdvance;
	}
}

void Main()
{
	const Font font(50);

	const String text = U"OpenSiv3D あいうえお 12345";

	while (System::Update())
	{
		DrawTextWithReflection(font, text, Vec2(40, 40), -5, HSV(40));
	}
}

4. 文字の後ろに図形

# include <Siv3D.hpp> // OpenSiv3D v0.6.2

class CharacterShape
{
private:

	Polygon m_polygon;

public:

	CharacterShape() = default;

	CharacterShape(const Font& font, const Glyph& glyph, double buffer)
	{
		Image image{ static_cast<size_t>(font.height()),  static_cast<size_t>(font.height()), Color{ 0, 0 } };

		font(glyph.codePoint).overwrite(image, Vec2{ 0, 0 });

		const MultiPolygon polygons = image.alphaToPolygons();

		Array<Vec2> points;

		for (const auto& polygon : polygons)
		{
			for (const auto& point : polygon.outer())
			{
				points << point;
			}
		}

		m_polygon = Geometry2D::ConvexHull(points)
			.calculateRoundBuffer(buffer);
	}

	void draw(const Vec2& pos, const ColorF& color) const
	{
		m_polygon.draw(pos, color);
	}
};

void DrawCharacterShapes(const Array<CharacterShape>& shapes,
	const Font& font, const String& text, const Vec2& pos, const ColorF& color, double margin = 0.0)
{
	const Vec2 basePos = pos;
	Vec2 penPos{ basePos };
	size_t i = 0;

	for (const auto& glyph : font.getGlyphs(text))
	{
		if (glyph.codePoint == U'\n')
		{
			penPos.x = basePos.x;
			penPos.y += font.height();
			continue;
		}

		shapes[i].draw(penPos, color);
		penPos.x += (glyph.xAdvance + margin);
		++i;
	}
}

void DrawTextWithMargin(const Font& font, const String& text, const Vec2& pos, const ColorF& color, double margin = 0.0)
{
	const Vec2 basePos = pos;
	Vec2 penPos{ basePos };

	for (const auto& glyph : font.getGlyphs(text))
	{
		if (glyph.codePoint == U'\n')
		{
			penPos.x = basePos.x;
			penPos.y += font.height();
			continue;
		}

		glyph.texture.draw(penPos + glyph.getOffset(), color);
		penPos.x += (glyph.xAdvance + margin);
	}
}

void Main()
{
	const Font font{ 66, Typeface::Heavy };
	const String text = U"あいうえお12345";
	constexpr double bufferWidth = 14;

	Array<CharacterShape> shapes;
	for (const auto& glyph : font.getGlyphs(text))
	{
		shapes.emplace_back(font, glyph, bufferWidth);
	}

	HSV shapeColor = Palette::Seagreen, textColor = Palette::White;
	double margin = 0.0;

	while (System::Update())
	{
		constexpr Vec2 pos{ 40, 40 };

		DrawCharacterShapes(shapes, font, text, pos, shapeColor, margin);
		DrawTextWithMargin(font, text, pos, textColor, margin);

		SimpleGUI::Slider(margin, 0.0, 20.0, Vec2{ 20, 160 });
		SimpleGUI::ColorPicker(shapeColor, Vec2{ 20, 200 });
		SimpleGUI::ColorPicker(textColor, Vec2{ 20, 320 });
	}
}
v0.4.3
# include <Siv3D.hpp> // OpenSiv3D v0.4.3

class CharacterShape
{
private:

	Polygon m_polygon;

public:

	CharacterShape() = default;

	CharacterShape(const Font& font, const Glyph& glyph, double buffer)
	{
		Image image(font.height(), font.height(), Color(0, 0));

		font(glyph.codePoint).overwrite(image);

		const MultiPolygon polygons = image.alphaToPolygons();

		Array<Vec2> points;

		for (const auto& polygon : polygons)
		{
			for (const auto& point : polygon.outer())
			{
				points << point;
			}
		}

		m_polygon = Geometry2D::ConvexHull(points)
			.calculateRoundBuffer(buffer);
	}

	void draw(const Vec2& pos, const ColorF& color) const
	{
		m_polygon.draw(pos, color);
	}
};

void DrawCharacterShapes(const Array<CharacterShape>& shapes,
	const Font& font, const String& text, const Vec2& pos, const ColorF& color, double margin = 0.0)
{
	const Vec2 basePos = pos;
	Vec2 penPos(basePos);
	size_t i = 0;

	for (const auto& glyph : font(text))
	{
		if (glyph.codePoint == U'\n')
		{
			penPos.x = basePos.x;
			penPos.y += font.height();
			continue;
		}

		shapes[i].draw(penPos, color);
		penPos.x += (glyph.xAdvance + margin);
		++i;
	}
}

void DrawTextWithMargin(const Font& font, const String& text, const Vec2& pos, const ColorF& color, double margin = 0.0)
{
	const Vec2 basePos = pos;
	Vec2 penPos(basePos);

	for (const auto& glyph : font(text))
	{
		if (glyph.codePoint == U'\n')
		{
			penPos.x = basePos.x;
			penPos.y += font.height();
			continue;
		}

		glyph.texture.draw(penPos + glyph.offset, color);
		penPos.x += (glyph.xAdvance + margin);
	}
}

void Main()
{
	const Font font(66, Typeface::Heavy);
	const String text = U"あいうえお12345";
	constexpr double bufferWidth = 14;

	Array<CharacterShape> shapes;
	for (const auto& glyph : font(text))
	{
		shapes.emplace_back(font, glyph, bufferWidth);
	}

	HSV shapeColor = Palette::Seagreen, textColor = Palette::White;
	double margin = 0.0;

	while (System::Update())
	{
		constexpr Vec2 pos(40, 40);

		DrawCharacterShapes(shapes, font, text, pos, shapeColor, margin);
		DrawTextWithMargin(font, text, pos, textColor, margin);

		SimpleGUI::Slider(margin, 0.0, 20.0, Vec2(20, 160));
		SimpleGUI::ColorPicker(shapeColor, Vec2(20, 200));
		SimpleGUI::ColorPicker(textColor, Vec2(20, 320));
	}
}

参考資料

Discussion