Open10

OpenSiv3D Challenge 2021

ピン留めされたアイテム
Ryo SuzukiRyo Suzuki

OpenSiv3D Challenge 2021 について

  • 開始日 2021 年 3 月 6 日

OpenSiv3D の新機能を作るいくつかのチャレンジ課題 の中から好きなものを選び、Siv3D ユーザコミュニティ Slack 内の専用のチャンネルに参加し、メンバーどうし協力 or 競争しながら実装の完成を目指すイベントです。優先的なコードレビューや実装のアドバイスなど、技術サポートを受けられます。

それぞれのチャレンジは、1 つの成果物が公式リポジトリにマージ可能な品質に到達するまで続きます。チャンネルにおいて何らかのアウトプットをした全員の名前が、OpenSiv3D 公式リポジトリのソースコード内に記載され、とくに顕著な貢献があった方はコミッタとして記録に残ります。

もし、ほかの参加者の実装が良さそうであっても、練習として自分なりに実装してオリジナルの成果をチャンネルに投稿してみることを推奨しています(貢献としてカウントされます)。

チャレンジを盛り上げるため、実装した内容などを #OpenSiv3D タグを使って Twitter に投稿しましょう。

一部の課題は開発中の OpenSiv3D v0.6 の機能を使うため、v0.6 での実装が必要です。v0.6 はまだインストーラが提供されていないため、公式リポジトリからソースコードをダウンロードし、自前でビルドする必要があります。OpenSiv3D | ライブラリの自前ビルドOpenSiv3D v0.6 サンプル を参照してください。

昨年 2020 年は 3 つのチャレンジを実施し、成果物がすべて OpenSiv3D v0.6 に実装されました。

Ryo SuzukiRyo Suzuki

Challenge 05 | Saphe2D::Squircle()

正方形と円形の中間の形状で、RoundRect よりもなめらかなカーブを持つ「Squircle」という形状があります。スマホアプリのアイコンなどで使われています。この形状を Shape2D で作成できるようにしてください。

実装方法

最終的には Shape2D::Hexagon() のように Shape2D の静的メンバ関数として実装しますが、そのためのリファレンス実装を、フリー関数(非メンバ関数)として Main.cpp に実装してください。

Shape2D頂点配列 Array<Float2>(三角形)インデックス配列 Array<TriangleIndex> (v0.4.3 以前は Array<uint16>) で構成されます。Shape2D における頂点配列は、図形を構成する頂点座標を時計回りに並べた配列で、後者はその何番目の頂点 (3 つ、時計回り) を組み合わせてポリゴンの一部を構成する三角形を描画するかを指定するデータです。例えば長方形であれば 4 つの頂点と 2 つの三角形インデックスで表現できます。五角形 (Shape2D::Pentagon()) であれば 5 つの頂点と 3 つの三角形インデックスで表現できます。

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

Shape2D Squircle()
{
	return{};
}

void Main()
{
	while (System::Update())
	{
		Squircle().draw();
	}
}

Siv3D Slack チャンネル

  • #ch05-squircle

参考コード

参考資料

より完成度を高めるために

  • なるべく計算量を小さくする
  • カーブの品質(頂点数)を引数で制御できるようにする
Ryo SuzukiRyo Suzuki

Challenge 06 | 吹き出し形状のコレクション

セリフや感情を表すときに使う、漫画的な吹き出しの形状を Shape2D の関数で簡単に作れるようにしたいです。すでに Shape2D::RectBalloon() がありますが、それ以外にもバリエーションを増やしたいです。

実装方法

関数名は最終実装時に再検討するので、適当で大丈夫です。

Shape2D頂点配列 Array<Float2>(三角形)インデックス配列 Array<TriangleIndex> (v0.4.3 以前は Array<uint16>) で構成されます。Shape2D における頂点配列は、図形を構成する頂点座標を時計回りに並べた配列で、後者はその何番目の頂点 (3 つ、時計回り) を組み合わせてポリゴンの一部を構成する三角形を描画するかを指定するデータです。例えば長方形であれば 4 つの頂点と 2 つの三角形インデックスで表現できます。五角形 (Shape2D::Pentagon()) であれば 5 つの頂点と 3 つの三角形インデックスで表現できます。

なお、Shape2DPolygonMultiPolygon と異なり、穴があったり離れ小島があったりしてはいけないという制約があります、💭 のように離れ小島がある吹き出しを作りたい場合は Array<Shape2D> を返してください。

(追記)
吹き出しの雲形状と、話者への矢印形状をそれぞれ独立した Shape2D として作成するのが筋が良さそうです。したがって、2 つの Shape2D を重ねて描画することで吹き出しを作る方法も推奨します。

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

Shape2D SpeechBalloo1()
{
	return{};
}

Shape2D SpeechBalloo2()
{
	return{};
}

void Main()
{
	while (System::Update())
	{
		SpeechBalloo1().draw();

		SpeechBalloo2().draw();
	}
}

Siv3D Slack チャンネル

  • #ch06-speechballoon

参考コード

参考資料

より完成度を高めるために-

  • なるべく計算量を小さくする
  • カーブの品質(頂点数)を引数で制御できるようにする
Ryo SuzukiRyo Suzuki

Challenge 07 | 国と都市

世界の国や日本の都道府県、市町村などの形状(輪郭)を Array<Vec2> または Array<Array<Vec2>> で取得し、Polygon, Array<Polygon> として描画したいです。そのためのデータベースと、データのパースの方法を見つけてください。データベースは緩やかなライセンスで公開されているものを利用してください。「国のみ」「市町村のみ」のデータベース・方法でも OK です。

都道府県のパズルを作ったり、物理演算で国をぶつけあったり(不穏)、面白いアプリケーションが作れそうな気がしませんか?

実装例

# include <Siv3D.hpp> // OpenSiv3D v0.4.3 または v0.6

Array<Polygon> LoadShape(const FilePath& path)
{
	return{};
}

void Main()
{
	const Array<Polygon> polygons = LoadShape(U"???.???");

	while (System::Update())
	{
		for (const auto& polygon : polygons)
		{
			polygon.draw();
		}
	}
}

Siv3D Slack チャンネル

  • #ch07-cityshape

参考コード

参考資料

より完成度を高めるために

Ryo SuzukiRyo Suzuki

Challenge 08 | 円グラフの描画

Siv3D に棒グラフや折れ線グラフがほしいというリクエストはかねてから寄せられています。しかしこれらのグラフは要素数が変わったりスケールが変わったり、数式のグラフであれば値域や精度を考慮する必要があるなど、実装難度が高いです。まずは比較的簡単なグラフで実装経験を積んでからステップアップしていきたいです。そこで円グラフです。入力は Stringdouble のペアの配列 Array<std::pair<String, double>> さえあれば最低限のものは作れるでしょう。最初は単純にパイの描画から実装してみましょう。そこに、ラベルやタイトルの表示、色分けなど、カスタマイズできる要素を追加していきましょう。どのような API (関数、クラス設計)であれば使いやすいでしょうか。一緒に考えていきましょう。なお、3D 円グラフ は宗教上の問題で NG です。

実装例

# include <Siv3D.hpp> // OpenSiv3D v0.4.3 または v0.6

void DrawPieChart()
{

}

class PieChart
{

};

void Main()
{
	while (System::Update())
	{

	}
}

Siv3D Slack チャンネル

  • #ch08-piechart

より完成度を高めるために

  • 指定した矩形内に収まるように描画したいです
  • 色分けがカラーバリアフリーに対応していると嬉しいです
Ryo SuzukiRyo Suzuki

Challenge 09 | 手書き数字・英字の認識

ユーザがマウスで描いた数字や英字を認識し、入力として使えると、脳トレ的な計算アプリや、英語練習アプリ、その他特殊な UI などに使えて便利です。

実装方法

ストローク (Array<LineString>) または画像 (Image) を入力にして、インタラクティブな速度で判定できることが望ましいです。結果の候補(複数)を probability とセットで取得できるのが望ましいです。クロスプラットフォームでなくなるためベストではありませんが、OS 固有の API を使う方法も OK です。なお、ユーザのストロークが入力であるという点で、OCR とは異なるものだと思います。

# include <Siv3D.hpp> // OpenSiv3D v0.4.3 または v0.6

struct Result
{
	char32 ch = U'\0';
	double probability = 0.0;
};

Array<Result> RecognizeCharacterFromStroke(const Array<LineString>& strokes, const Rect& area)
{
	return{};
}

void Main()
{
	while (System::Update())
	{

	}
}

Siv3D Slack チャンネル

  • #ch09-character-recognition

より完成度を高めるために

  • 実在しない文字についても学習させて認識できるようになると面白そうです
Ryo SuzukiRyo Suzuki

Challenge 10 | OutlineGlyph to Array<Polygon>

OpenSiv3D v0.6 の Font::renderOutline(ch) を使うと、指定した文字の輪郭を Array<LineString> として取得できます。しかしこれらの LineString がどう組み合わさって Polygon を構成するかはわからないので、Array<Polygon> を得ようとすると追加の処理が必要になります。この処理を効率的に実装してください。

「A」という字は 1 個の穴を持つ Polygon 1 つ、「B」という字は 2 個の穴を持つ Polygon 1 つです。「品」という字はそれぞれ 1 個の穴を持つ Polygon が 3 つです。

実装するにあたって

  • 入力として与えられる、文字の Array<LineString> の 1 つ 1 つの LineString については、外側の輪郭である場合は時計回り (CW) 、穴になる場合は反時計回り (CCW) であることが決まっていますが、LineString がどのような順番であるかはランダムです
  • 点群(で構成される Ring) が時計回りであるかは、Geometry2D::IsClockwise() で調べられます
  • Polygon のコンストラクタに渡す頂点配列は、外側は時計回り、穴は反時計回りである必要があります
# include <Siv3D.hpp> // OpenSiv3D v0.6

// Array<LineString> を Array<Polygon> に変換する関数を実装してください
Array<Polygon> ToPolygons(const Array<LineString>& rings)
{
	for (const auto& ring : rings)
	{
		if (Geometry2D::IsClockwise(ring))
		{
			// 時計回りは外側
			Console << U"CW";
		}
		else
		{
			// 反時計回りは穴
			Console << U"CCW";
		}

		Console << ring;
	}

	Array<Polygon> polygons;

	// 外側(時計回り)
	const LineString outer{ {200, 200}, {400, 200},{400, 400}, {200, 400} };

	// 穴(反時計回り)
	const LineString hole{ {250, 250}, {250, 350},{350, 350}, {350, 250} };

	// Polygon{ const Array<Vec2>& outer, const Array<Array<Vec2>>& holes };
	const Polygon polygon{ outer, { hole } };

	polygons << polygon;

	return polygons;
}

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

	// U'回', U'品', U'晶', U'照', U'豊'
	const Array<LineString> rings = font.renderOutline(U'A').rings;

	const Array<Polygon> polygons = ToPolygons(rings);

	while (System::Update())
	{
		for (const auto& ring : rings)
		{
			ring.drawClosed();
		}

		for (const auto& polygon : polygons)
		{
			polygon.draw(ColorF{ 1, 0.5, 0.0, 0.25 });
		}

		PutText(Format(Cursor::Pos()), Vec2{ 400, 20 });
	}
}

Siv3D Slack チャンネル

  • #ch10-outlineglyph

より完成度を高めるために

  • なるべく計算量を小さくする
Ryo SuzukiRyo Suzuki

Challenge 12 | Photon を使ったオンライン対戦ゲーム開発の資料整備

Photon SDK を使うと、C++/Siv3D でオンライン対戦ゲームの開発ができるようになります。すでに Photon + OpenSiv3D Web 版 で オンライン将棋対戦ゲーム を完成させた @mak1a_ctrl さんと協力しながら、導入のための易しいドキュメントや複数のサンプルの作成、あらゆるプラットフォームでの動作検証等に取り組み、より多くの人が OpenSiv3D でオンラインゲームを気軽に開発できるようにしてください。

実装方法

  • ドキュメントやサンプルコードをテキストまたは Markdown で記述

Siv3D Slack チャンネル

  • #ch12-photon

参考コード

参考資料

より完成度を高めるために

Ryo SuzukiRyo Suzuki

Challenge 13 | ネットワーク(グラフ)の描画

頂点と辺の情報が与えられた時、ネットワーク (数学や競技プログラミングではグラフと言われるもの) を手軽に描画できるようにしたいです。この実装にあたっては頂点の配置を自動的に決定するアルゴリズムを実装する必要がありますが、とりあえずは Fruchterman-Reingold アルゴリズムと呼ばれるものを実装できると良いと思います。

グラフの可視化は様々な方法があるので、1 つの汎用的な関数・クラスを作るよりも、見せ方や目的に応じた関数・クラスを用意できると良さそうです。

Siv3D Slack チャンネル

  • #ch13-network-visualization

参考資料

より完成度を高めるために

  • なるべく計算量を小さくする
  • Fruchterman-Reingold アルゴリズム以外のアルゴリズムを選択できるような設計の余地を残す
Ryo SuzukiRyo Suzuki

Challenge 14 | 手書き図形の認識

ユーザがマウスで描いた図形を認識し、LineCircle, RectF に変換できると、画面に図形を描いてオブジェクトを配置するような直感的な UI の実装に使えて便利です。

実装方法

ストローク (LineString) を入力として、リアルタイムの速度で判定できることが望ましいです。結果は std::variant で返します。ぐちゃぐちゃな線や歪んだ図形、短すぎる線など、図形でない / ノイズと判断されるものは return{};std::monostate を返します。

ユーザーが雑に書いた円は、曲線が閉じているとは限りません。なるべくユーザが描こうとしていた図形に近い形状を提示できるようにしましょう。

何でもかんでも図形と判断するのではなく、図形ではなさそうな入力を「図形でない」としっかり判定できることも重要です。

# include <variant>
# include <Siv3D.hpp> // OpenSiv3D v0.6

namespace s3d
{
	// - 検出の閾値など、パラメータを追加してよい
	// - 可能だったら Triangle, Quad, Bezier2 もオプションで追加判定できるようにしてもよい
	// Param param{ .minLineLength = 10.0, .minCircleR = 5.0, .xxxxThreshold = ... };
	// bool enableTriangle = true, enableQuad = false, enableBezier2;
	// auto result = ToShape(points, param, enableTriangle, enableQuad, enableBezier2);

	using Result = std::variant<std::monostate, Line, Circle, RectF>;
	// using Result = std::variant<std::monostate, Line, Circle, RectF, Triangle, Quad, Bezier2>;

	/// @brief LineString が図形を描いている場合、その図形を返す。
	/// @param points LineString
	/// @param minSize 図形の最小サイズ(Line の長さ、円の直径、長方形の縦と横の長さの合計などに使う?)
	/// @return LineString が表現する図形。判定不能は std::monostate
	Result ToShape(const LineString& points, double minSize = 10.0)
	{
		if (points.size() < 2) // 頂点数が 1 しかない場合は判定不能
		{
			return{}; // std::monostate
		}

		// LineString の全長
		const double length = points.calculateLength();

		if (length < minSize) // 線の全長が 10 以下の場合は判定不能
		{
			return{}; // std::monostate
		}

		return Line{ points.front(), points.back() };
	}
}

void Main()
{
	Scene::SetBackground(ColorF{ 0.96 });

	LineString points;

	Result result;

	while (System::Update())
	{
		// 線を引く
		if (MouseL.down()
			|| (MouseL.pressed() && !Cursor::DeltaF().isZero()))
		{
			if (MouseL.down())
			{
				// 書いた線と判定結果をクリア
				points.clear();
				result = std::monostate{};
			}

			points << Cursor::PosF();
		}

		// 書いた線と判定結果をクリア
		if ((MouseR | KeySpace).down())
		{
			points.clear();
			result = std::monostate{};
		}

		// 線を引き終わったら判定
		if (points && MouseL.up())
		{
			result = ToShape(points);
		}

		// 線の描画
		points.draw(4, ColorF{ result.index() ? 0.75 : 0.25 });

		switch (result.index()) // variant が持ってる型に応じて switch
		{
		case 0:
			break;
		case 1:
			std::get<Line>(result).draw(3, Palette::Red);
			break;
		case 2:
			std::get<Circle>(result).drawFrame(3, Palette::Red);
			break;
		case 3:
			std::get<RectF>(result).drawFrame(3, Palette::Red);
			break;
		}
	}
}

お役立ち情報

  • OpenSiv3D v0.6 で追加された Polygon::CorrectOne(points) は、自己交差・反時計回りなど Polygon を作るには不正な頂点配列の入力であっても、うまく解釈して 1 つの有効な Polygon を作成してくれます。この関数はこのチャレンジで役立つかもしれません
  • その関数が返したポリゴンの外周をPolygon::outer()Array<Vec2> として取得し、それについて判定を行うのが、ノイズが少なくて良いように思います
# include <Siv3D.hpp> // OpenSiv3D v0.6

void Main()
{
	Scene::SetBackground(ColorF{ 0.96 });

	LineString points;

	Polygon polygon;

	while (System::Update())
	{
		// 線を引く
		if (MouseL.down()
			|| (MouseL.pressed() && !Cursor::DeltaF().isZero()))
		{
			if (MouseL.down())
			{
				// 書いた線と判定結果をクリア
				points.clear();
				polygon = Polygon{};
			}

			points << Cursor::PosF();
		}

		// 書いた線と判定結果をクリア
		if ((MouseR | KeySpace).down())
		{
			points.clear();
			polygon = Polygon{};
		}

		// 線を引き終わったら判定
		if (points && MouseL.up())
		{
			// 交差するなど不正な頂点配列でも、うまく解釈して Polygon を作成
			polygon = Polygon::CorrectOne(points);
		}

		// 線の描画
		points.draw(4, ColorF{ polygon ? 0.75 : 0.25 });

		// ポリゴンの描画
		polygon.draw(ColorF{ 1.0, 0.0, 0.0, 0.5 });
	}
}

ポイント

  • LineStringArray<Vec2> とほぼ同じインタフェースを持ちます (operator[], .size() など)
  • size_t LineString::num_lines(), Line LineString::line(i) も便利です
  • ユーザーが反時計回りに三角形を描いても、関数が返す Triangle は時計回りで作成してください(OpenSiv3D の Triangle の基本ルール)
  • 一方で、Line はユーザが描いたのと同じ方向を持つと便利だと思います
  • LineString::simplified()LineString を単純化すると計算コストが減るかもしれません
  • 自己交差 (各 Line について交差判定)が多い場合不正な図形である可能性が高そうです
  • LineLineString のメンバ関数に、便利な関数があるかもしれません
  • 「こういう関数はありますか?」「この関数は何ですか?」という質問をお気軽に Slack でしてください
  • 使えるかもしれない関数: Geometry2D::HausdorffDistance() ハウスドルフ距離, Geometry2D::FrechetDistance() (フレシェ距離)

Siv3D Slack チャンネル

  • #ch14-shape-recognition

より完成度を高めるために

  • 指定したいくつかの図形タイプのみ判定する、誤差をどこまで許容する、これより小さいサイズは判定対象外にする、などパラメータを柔軟に設定できる(専用の struct を用意する)と、様々なユースケースに対応できると思います