Open63

OpenSiv3D v0.6 開発ログ

Ryo SuzukiRyo Suzuki

KDTree

  • v0.4.3 よりも短いコードで書けるように
# include <Siv3D.hpp> // OpenSiv3D v0.6

struct Unit
{
	Circle circle;

	ColorF color;

	void draw() const
	{
		circle.draw(color);
	}
};

// Unit を KDTree で扱えるようにするためのアダプタ
struct UnitAdapter : KDTreeAdapter<Array<Unit>, Vec2>
{
	static const element_type* GetPointer(const point_type& point)
	{
		return point.getPointer();
	}

	static element_type GetElement(const dataset_type& dataset, size_t index, size_t dim)
	{
		return dataset[index].circle.center.elem(dim);
	}
};

void Main()
{
	// 200 個の Unit を生成
	Array<Unit> units;
	{
		for (size_t i = 0; i < 200; ++i)
		{
			Unit unit
			{
				.circle = Circle{ RandomVec2(Scene::Rect()), 4 },
				.color = RandomColorF()
			};
			units << unit;
		}
	}

	// kd-tree を構築
	KDTree<UnitAdapter> kdTree{ units };

	// radius search する際の探索距離
	constexpr double searchDistance = 80.0;

	while (System::Update())
	{
		const Vec2 cursorPos = Cursor::PosF();

		Circle{ cursorPos, searchDistance }.draw(ColorF{ 1.0, 0.2 });

		// searchDistance 以内の距離にある Unit のインデックスを取得
		for (auto index : kdTree.radiusSearch(cursorPos, searchDistance))
		{
			Line{ cursorPos, units[index].circle.center }.draw(4);
		}

		// ユニットを描画
		for (const auto& unit : units)
		{
			unit.draw();
		}
	}
}
Ryo SuzukiRyo Suzuki

Spline2D

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

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

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

	Array<Vec2> points;
	Spline2D spline;

	Polygon polygon;
	Stopwatch stopwatch;
	SplineIndex si;

	while (System::Update())
	{
		// 制御点の追加
		if (MouseL.down())
		{
			points << Cursor::Pos();
			spline = Spline2D{ points, CloseRing::Yes };
			polygon = spline.calculateRoundBuffer(24);
			stopwatch.restart();
		}

		// 各区間の Bounding Rect の可視化
		for (size_t i = 0; i < spline.size(); ++i)
		{
			const ColorF color = Colormap01F(i / 18.0);
			spline.boundingRect(i)
				.draw(ColorF{ color, 0.1 })
				.drawFrame(1, 0, ColorF{ color, 0.5 });
		}

		// 点を追加してから 1 秒間は三角形分割を表示
		if (stopwatch.isRunning()
			&& (stopwatch < 1s))
		{
			polygon.drawWireframe(1, ColorF{ 0.25, (1.0 - stopwatch.sF()) });
			polygon.draw(ColorF{ 0.4, stopwatch.sF() });
		}
		else
		{
			polygon.draw(ColorF{ 0.4 });
			// 曲率に応じた色でスプラインを描画
			spline.draw(10, [&](SplineIndex si) { return Colormap01F(spline.curvature(si) * 24); });
		}

		// 制御点の表示
		for (const auto& point : points)
		{
			Circle{ point, 8 }.drawFrame(2, ColorF{ 0.8 });
		}

		// スプライン上を移動する物体
		if (spline)
		{
			si = spline.advanceWrap(si, Scene::DeltaTime() * 400);
			Circle{ spline.position(si), 20 }.draw(HSV{ 145, 0.9, 0.95 });
		}
	}
}
Ryo SuzukiRyo Suzuki

長方形詰込み(回転許可)

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

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

// ランダムな長方形の配列を作成
Array<Rect> GenerateRandomRects()
{
    Array<Rect> rects(Random(4, 32));

    for (auto& rect : rects)
    {
        const Point pos = RandomPoint(Rect{ Scene::Size() - Size{ 150, 150 } });
        rect.set(pos, Random(20, 150), Random(20, 150));
    }

    return rects;
}

void Main()
{
    Window::Resize(1280, 720);
    Scene::SetBackground(ColorF{ 0.99 });
    Array<Rect> input;
    Array<double> rotations;
    RectanglePack output;
    Point offset{ 0, 0 };
    Stopwatch s;

    while (System::Update())
    {
        if (!s.isStarted() || s > 1.8s)
        {
            input = GenerateRandomRects();
            rotations.resize(input.size());
            rotations.fill(0.0);
            output = RectanglePacking::Pack(input, 1024, AllowFlip::Yes);

            for (size_t i = 0; i < input.size(); ++i)
            {
                if (input[i].w != output.rects[i].w)
                {
                    rotations[i] = 270_deg;
                }
            }

            // 画面中央に表示するよう位置を調整
            offset = (Scene::Size() - output.size) / 2;
            for (auto& rect : output.rects)
            {
                rect.moveBy(offset);
            }

            s.restart();
        }

        // アニメーション
        const double k = Min(s.sF() * 10, 1.0);
        const double t = Math::Saturate(s.sF() - 0.2);
        const double e = EaseInOutExpo(t);

        Rect{ offset, output.size }.draw(ColorF{ 0.7, e });

        for (auto i : step(input.size()))
        {
            const RectF in = input[i].scaledAt(input[i].center(), k);
            const RectF out = output.rects[i];
            const Vec2 center = in.center().lerp(out.center(), e);           
            const RectF rect{ Arg::center = center, in.size };

            rect.rotatedAt(rect.center(), Math::Lerp(0.0, rotations[i], e))
                .draw(HSV{ i * 25.0, 0.65, 0.9 })
                .drawFrame(2, 0, ColorF{ 0.25 });
        }
    }
}
Ryo SuzukiRyo Suzuki

Subdivision2D

  • v0.4.3 よりも短いコードで書けるように
# include <Siv3D.hpp> // OpenSiv3D v0.6

void Main()
{
	Window::Resize(1280, 720);

	constexpr Size size{ 1280, 720 };
	constexpr Rect rect{ size };
	Subdivision2D subdiv{ rect };

	for (const PoissonDisk2D pd{ size, 40 }; 
		const auto& point : pd.getPoints())
	{
		if (rect.contains(point))
		{
			subdiv.addPoint(point);
		}
	}

	const Array<Polygon> facetPolygons = subdiv
		.calculateVoronoiFacets()
		.map([rect](const VoronoiFacet& f)
	{
		return Geometry2D::And(Polygon{ f.points }, rect).front();
	});

	while (System::Update())
	{
		for (auto [i, facetPolygon] : Indexed(facetPolygons))
		{
			facetPolygon
				.draw(HSV{ i * 25.0, 0.65, 0.8 })
				.drawFrame(2, ColorF{ 0.25 });
		}
	}
}
Ryo SuzukiRyo Suzuki

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

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

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

	Array<Vec2> points;
	Polygon polygon;
	LineString path;

	constexpr NavMeshConfig config{ .agentRadius = 20.0 };
	NavMesh navMesh;

	while (System::Update())
	{
		if (MouseL.down())
		{
			points << Cursor::Pos();
			polygon = Spline2D{ points }.calculateRoundBuffer(24, 8, 12);
			navMesh.build(polygon, config);
			path = navMesh.query(points.front(), points.back());
		}

		polygon.draw(ColorF{ 1.0 }).drawFrame(2, ColorF{ 0.7 });

		if (path)
		{
			path.draw(8, ColorF{ 0.1, 0.5, 0.9 });
			path.front().asCircle(12).draw(ColorF{ 1.0, 0.3, 0.0 });
			path.back().asCircle(12).draw(ColorF{ 1.0, 0.3, 0.0 });
		}
	}
}
Ryo SuzukiRyo Suzuki

LineString::calculatePointFromOrigin()

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

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

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

	LineString points;
	Polygon polygon;

	double distanceFromOrigin = 0.0;
	double length = 0.0;

	while (System::Update())
	{
		if (MouseL.down())
		{
			points << Cursor::Pos();
			polygon = points.calculateRoundBuffer(20);
			length = points.calculateLength();
		}

		polygon.draw().drawFrame(2, ColorF{ 0.7 });
		points.draw(2, ColorF{ 0.75 });

		if (2 < points.size() && length)
		{
			distanceFromOrigin += (Scene::DeltaTime() * 800);

			if (length < distanceFromOrigin)
			{
				distanceFromOrigin = Math::Fmod(distanceFromOrigin, length);
			}

			const Vec2 position = points.calculatePointFromOrigin(distanceFromOrigin);
			position.asCircle(20).draw(ColorF{ 0.5 });
		}
	}
}
Ryo SuzukiRyo Suzuki

Polygon::outline()

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

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

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

	const Polygon polygon0 = Shape2D::Plus(180, 100, Scene::Center().movedBy(-350, -120));
	const Polygon polygon1 = Shape2D::Heart(180, Scene::Center().movedBy(0, 120));
	const Polygon polygon2 = Shape2D::NStar(8, 180, 140, Scene::Center().movedBy(350, -120));

	while (System::Update())
	{
		const double t = (Scene::Time() * 720);

		polygon0.draw(ColorF{ 0.4 });
		polygon0.outline(t, 200).draw(LineStyle::RoundCap, 8, ColorF{ 0, 1, 0.5 });

		polygon1.draw(ColorF{ 0.4 });
		polygon1.outline(t, 200).draw(LineStyle::RoundCap, 8, ColorF{ 0, 1, 0.5 });
		
		polygon2.draw(ColorF{ 0.4 });
		polygon2.outline(t, 200).draw(LineStyle::RoundCap, 8, ColorF{ 0, 1, 0.5 });
	}
}
Ryo SuzukiRyo Suzuki

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

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

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

	constexpr Size size{ 1280, 720 };
	constexpr Rect rect{ size };
	Subdivision2D subdiv{ rect };

	for (const PoissonDisk2D pd{ size, 40 };
		const auto & point : pd.getPoints())
	{
		if (rect.contains(point))
		{
			subdiv.addPoint(point);
		}
	}

	const Array<Polygon> facetPolygons = subdiv
		.calculateVoronoiFacets()
		.map([rect](const VoronoiFacet& f)
			{
				return Geometry2D::And(Polygon{ f.points }, rect).front();
			});

	Polygon mesh;
	NavMesh navMeshL, navMeshS;
	LineString pathL, pathS;
	constexpr double agentRadiusL = 30, agentRadiusS = 10;
	const Vec2 lStart{ 140, 120 };
	const Vec2 sStart{ 100, 200 };
	const Vec2 goal{ 1100, 300 };
	const Polygon goalDiamond = RectF{ Arg::center = goal, 48 }.rotated(45_deg).calculateRoundBuffer(3);

	while (System::Update())
	{
		for (const auto& facetPolygon : facetPolygons)
		{
			facetPolygon
				.draw(ColorF{ 0.3 })
				.drawFrame(2, ColorF{ 0.25 });
		}

		if (MouseL.pressed())
		{
			const Vec2 pos = Cursor::Pos();

			for (const auto& facetPolygon : facetPolygons)
			{
				if (facetPolygon.intersects(pos))
				{
					if (mesh.isEmpty())
					{
						mesh = facetPolygon;
					}
					else
					{
						mesh.append(facetPolygon);
						navMeshL.build(mesh, NavMeshConfig{ .agentRadius = agentRadiusL });
						navMeshS.build(mesh, NavMeshConfig{ .agentRadius = agentRadiusS });
						pathL = navMeshL.query(lStart, goal);
						pathS = navMeshS.query(sStart, goal);
					}

					break;
				}
			}
		}

		mesh.draw(ColorF{ 0.9, 0.8, 0.6 }).drawFrame(6, ColorF{ 0.5, 0.3, 0.0 });

		lStart.asCircle(agentRadiusL).draw(ColorF{ 0.2, 0.6, 0.5 });
		pathL.draw(6, ColorF{ 0.2, 0.6, 0.5 });

		sStart.asCircle(agentRadiusS).draw(ColorF{ 0.2, 0.3, 0.5 });
		pathS.draw(6, ColorF{ 0.2, 0.3, 0.5 });

		goalDiamond.draw(ColorF{ 0.9, 0.2, 0.0 });
	}
}
Ryo SuzukiRyo Suzuki

Graphics2D::DrawTriangles()

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

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

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
	
	const VertexShader vs
		= HLSL{ U"example/shader/hlsl/soft_shape.hlsl" }
		| GLSL{ U"example/shader/glsl/soft_shape.vert", { { U"VSConstants2D", 0 }, { U"SoftShape", 1 } }};

	if (!vs)
	{
		throw(U"Failed to load shader file.");
	}

	ConstantBuffer<float> cb;

	while (System::Update())
	{
		cb = static_cast<float>(Scene::Time());
		Graphics2D::SetConstantBuffer(ShaderStage::Vertex, 1, cb);
		
		{
			ScopedCustomShader2D shader{ vs };
			Graphics2D::DrawTriangles(360);
		}
	}
}
Ryo SuzukiRyo Suzuki

SVG

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

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

void Main()
{
	const Grid<bool> qr = QR::EncodeText(U"https://github.com/Siv3D/OpenSiv3D/tree/v6_winmac_develop");	
	const SVG svg = QR::MakeSVG(qr);
	const Texture t1{ svg.render(100, 100) };
	const Texture t2{ svg.render(200, 200) };
	const Texture t3{ svg.render(800, 800) };

	while (System::Update())
	{
		t1.draw();
		t2.draw(120, 0);
		t3.draw(340, 0);
	}
}
Ryo SuzukiRyo Suzuki

Effect の再帰

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

# include <Siv3D.hpp>

// 重力加速度
constexpr Vec2 Gravity{ 0, 240 };

// 火花の状態
struct Fire
{
	// 初速
	Vec2 v0;

	// 色相のオフセット
	double hueOffset;

	// スケーリング
	double scale;

	// 破裂するまでの時間
	double nextFireSec;

	// 破裂して子エフェクトを作成したか
	bool hasChild = false;
};

// 火花エフェクト
struct Firework : IEffect
{
	// 火花の個数
	static constexpr int32 FireCount = 12;

	// 循環参照を避けるため、IEffect の中で Effect を持つ場合、参照またはポインタにすること
	const Effect& m_parent; 
	
	// 花火の中心座標
	Vec2 m_center;
		
	// 火の状態
	std::array<Fire, FireCount> m_fires;

	// 何世代目? [0, 1, 2]
	int32 m_n;

	Firework(const Effect& parent, const Vec2& center, int32 n, const Vec2& v0)
		: m_parent{ parent }
		, m_center{ center }
		, m_n{ n }
	{
		for (auto i : step(FireCount))
		{
			const double angle = (i * 30_deg + Random(-10_deg, 10_deg));
			const double speed = (60.0 - m_n * 15) * Random(0.9, 1.1) * (IsEven(i) ? 0.5 : 1.0);
			m_fires[i].v0			= Circular{ speed, angle } + v0;
			m_fires[i].hueOffset	= Random(-10.0, 10.0) + (IsEven(i) ? 15 : 0);
			m_fires[i].scale		= Random(0.8, 1.2);
			m_fires[i].nextFireSec	= Random(0.7, 1.0);
		}
	}

	bool update(double t) override
	{
		for (const auto& fire : m_fires)
		{
			const Vec2 pos = m_center + fire.v0 * t + 0.5 * t * t * Gravity;
			pos.asCircle((10 - (m_n * 3)) * ((1.5 - t) / 1.5) * fire.scale)
				.draw(HSV{ 10 + m_n * 120.0 + fire.hueOffset, 0.6, 1.0 - m_n * 0.2 });
		}

		if (m_n < 2) // 0, 1 世代目なら
		{
			for (auto& fire : m_fires)
			{
				if (!fire.hasChild && (fire.nextFireSec <= t))
				{
					// 子エフェクトを作成
					const Vec2 pos = m_center + fire.v0 * t + 0.5 * t * t * Gravity;
					m_parent.add<Firework>(m_parent, pos, (m_n + 1), fire.v0 + (t * Gravity));
					fire.hasChild = true;
				}
			}
		}

		return (t < 1.5);
	}
};

// 打ち上げエフェクト
struct FirstFirework : IEffect
{
	// 循環参照を避けるため、IEffect の中で Effect を持つ場合、参照またはポインタにすること
	const Effect& m_parent;

	// 打ち上げ位置
	Vec2 m_start;

	// 打ち上げ初速
	Vec2 m_v0;

	FirstFirework(const Effect& parent, const Vec2& start, const Vec2& v0)
		: m_parent{ parent }
		, m_start{ start }
		, m_v0{ v0 } {}

	bool update(double t) override
	{
		const Vec2 pos = m_start + m_v0 * t + 0.5 * t * t * Gravity;
		Circle{ pos, 6 }.draw();
		Line{ m_start, pos }.draw(LineStyle::RoundCap, 8, ColorF{ 0.0 }, ColorF{ 1.0 - (t / 0.6) });

		if (t < 0.6)
		{
			return true;
		}
		else
		{
			// 終了間際に子エフェクトを作成
			const Vec2 velocity = m_v0 + t * Gravity;
			m_parent.add<Firework>(m_parent, pos, 0, velocity);
			return false;
		}
	}
};

void Main()
{
	Window::Resize(1280, 720);
	Effect effect;

	while (System::Update())
	{
		Scene::Rect().draw(Arg::top(0.0), Arg::bottom(0.2, 0.1, 0.4));

		if (MouseL.down())
		{
			effect.add<FirstFirework>(effect, Cursor::Pos(), Vec2{ 0, -500 });
		}

		{
			ScopedRenderStates2D blend{ BlendState::Additive };
			effect.update();
		}
	}
}
Ryo SuzukiRyo Suzuki

OutlineGlyph

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

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

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

	const Font font{ 130, U"example/font/RocknRoll/RocknRollOne-Regular.ttf" };
	const Array<OutlineGlyph> glyphs = font.renderOutlines(U"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!?");

	while (System::Update())
	{
		const double t = Periodic::Sawtooth0_1(2.6s);
		const double len = Periodic::Sine0_1(16s) * 0.5;
		constexpr Vec2 basePos{ 70, 0 };
		Vec2 penPos{ basePos };

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

			for (const auto& ring : glyph.rings)
			{
				const double length = ring.calculateLength(CloseRing::Yes);
				LineString z1 = ring.extractLineString(t * length, length * len, CloseRing::Yes);
				const LineString z2 = ring.extractLineString((t + 0.5) * length, length * len, CloseRing::Yes);
				z1.append(z2.reversed()).drawClosed(3, ColorF{ 0.25 });
			}

			if (penPos.x += glyph.xAdvance;
				1120 < penPos.x)
			{
				penPos.x = basePos.x;
				penPos.y += font.fontSize();
			}
		}
	}
}
Ryo SuzukiRyo Suzuki

JSON

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

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

JSON MakeJSON()
{
	JSON json;
	json[U"Window"][U"title"] = U"MyGame";
	json[U"Window"][U"width"] = 1280;
	json[U"Window"][U"height"] = 720;
	json[U"Game"][U"score"] = { 10, 20, 50, 100 };
	return json;
}

void Main()
{
	MakeJSON().save(U"save.json");

	JSON json = JSON::Load(U"save.json");
	json[U"Window"][U"width"] = 1920;
	json[U"Window"][U"height"] = 1080;
	Console << json;

	Console << U"-------------";
	Console << json[U"Window"][U"title"].get<String>();
	Console << json[U"Window"][U"width"].get<int32>();

	Console << U"----";

	// by index
	{
		const size_t size = json[U"Game"][U"score"].size();
		for (size_t i = 0; i < size; ++i)
		{
			Console << json[U"Game"][U"score"][i].get<int32>();
		}
	}

	Console << U"----";

	// range based
	{
		for (const auto& elem : json[U"Game"][U"score"].arrayView())
		{
			Console << elem.get<int32>();
		}
	}

	while (System::Update())
	{

	}
}
Ryo SuzukiRyo Suzuki

FontMethod

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

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

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

	const FilePath filePath = U"example/font/RocknRoll/RocknRollOne-Regular.ttf";
	const Font font1{ FontMethod::Bitmap, 48, filePath };
	const Font font2{ FontMethod::SDF, 48, filePath };
	const Font font3{ FontMethod::MSDF, 40, filePath };

	while (System::Update())
	{
		const ColorF textColor{ 0.25 };
		const double size = 140;

		font1(U"Bitmap")
			.draw(size, Vec2{ 20, 20 }, textColor);

		font2(U"SDF")
			.draw(size, Vec2{ 20, 220 }, textColor);

		font3(U"MSDF")
			.draw(size, Vec2{ 20, 420 }, textColor);
	}
}
Ryo SuzukiRyo Suzuki

CJK 対応標準フォント

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

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

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

	const Font fontJP{ 60, Typeface::CJK_Regular_JP };
	const Font fontKR{ 60, Typeface::CJK_Regular_KR };
	const Font fontSC{ 60, Typeface::CJK_Regular_SC };
	const Font fontTC{ 60, Typeface::CJK_Regular_TC };
	const Font fontHK{ 60, Typeface::CJK_Regular_HK };

	const String text = U"骨曜喝愛遙扇 - ";

	while (System::Update())
	{
		fontJP(U"こんにちは 你好 안녕하세요")
			.drawBase(20, 60, ColorF{ 0.25 });

		fontJP(text + U"JP").drawBase(20, 160, ColorF{ 0.25 });
		fontKR(text + U"KR").drawBase(20, 260, ColorF{ 0.25 });
		fontSC(text + U"SC").drawBase(20, 360, ColorF{ 0.25 });
		fontTC(text + U"TC").drawBase(20, 460, ColorF{ 0.25 });
		fontHK(text + U"HK").drawBase(20, 560, ColorF{ 0.25 });
	}
}
Ryo SuzukiRyo Suzuki

Material Design Icons

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

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

void Main()
{
	const Font font{ 30, Typeface::Medium };
	const Font iconFont{ 30, Typeface::Icon_MaterialDesign };
	font.addFallback(iconFont);

	const Array<String> items = {
		U"\xF00E8  更新",
		U"\xF11E8  編集",
		U"\xF09FA  キーボード設定",
		U"\xF173B  操作方法"
	};

	while (System::Update())
	{
		for (auto [i, item] : Indexed(items))
		{
			const RoundRect rect{ 40, 40 + i * 80.0, 320, 60, 8 };

			rect.draw(ColorF{ 0.99, 0.96, 0.93 });

			font(item)
				.draw(55, 48 + i * 80.0, ColorF{ 0.25 });
		}
	}
}
Ryo SuzukiRyo Suzuki

新しい絵文字のサポート

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

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

void Main()
{
	Scene::SetBackground(ColorF{ 0.4, 0.5, 0.6 });

	const Texture e0{ U"🪨"_emoji };
	const Texture e1{ U"🧋"_emoji };
	const Texture e2{ U"🪙"_emoji };
	const Texture e3{ U"🪜"_emoji };
	const Texture e4{ U"🪟"_emoji };
	const Texture e5{ U"🪵"_emoji };
	const Texture e6{ U"🪴"_emoji };
	const Texture e7{ U"🪣"_emoji };
	const Texture e8{ U"🪦"_emoji };
	const Texture e9{ U"🪧"_emoji };
	const Texture e10{ U"🪑"_emoji };
	const Texture e11{ U"🧊"_emoji };

	while (System::Update())
	{
		e0.drawAt(100, 100);
		e1.drawAt(300, 100);
		e2.drawAt(500, 100);
		e3.drawAt(700, 100);
		e4.drawAt(100, 300);
		e5.drawAt(300, 300);
		e6.drawAt(500, 300);
		e7.drawAt(700, 300);
		e8.drawAt(100, 500);
		e9.drawAt(300, 500);
		e10.drawAt(500, 500);
		e11.drawAt(700, 500);
	}
}
Ryo SuzukiRyo Suzuki

Buffer2D

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

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

void Main()
{
	const Texture texture{ U"🥺"_emoji };

	while (System::Update())
	{
		const double angle = (Scene::Time() * 30_deg);

		Shape2D::Pentagon(60, Vec2{ 100, 100 }, angle)
			.toBuffer2D(Arg::center(100, 100), texture.size())
			.draw(texture);

		Shape2D::Ngon(7, 60, Vec2{ 300, 100 }, angle)
			.toBuffer2D(Arg::center(300, 100), texture.size())
			.draw(texture);

		Shape2D::Plus(60, 30, Vec2{ 500, 100 }, angle)
			.toBuffer2D(Arg::center(500, 100), texture.size())
			.draw(texture);

		Shape2D::Heart(60, Vec2{ 700, 100 }, angle)
			.toBuffer2D(Arg::center(700, 100), texture.size())
			.draw(texture);


		Shape2D::Pentagon(60, Vec2{ 100, 300 }, angle)
			.toBuffer2D(Arg::center(100, 300), texture.size(), angle)
			.draw(texture);

		Shape2D::Ngon(7, 60, Vec2{ 300, 300 }, angle)
			.toBuffer2D(Arg::center(300, 300), texture.size(), angle)
			.draw(texture);

		Shape2D::Plus(60, 30, Vec2{ 500, 300 }, angle)
			.toBuffer2D(Arg::center(500, 300), texture.size(), angle)
			.draw(texture);

		Shape2D::Heart(60, Vec2{ 700, 300 }, angle)
			.toBuffer2D(Arg::center(700, 300), texture.size(), angle)
			.draw(texture);


		Shape2D::Pentagon(60, Vec2{ 100, 500 })
			.toBuffer2D(Arg::center(100, 500), texture.size(), angle)
			.draw(texture);

		Shape2D::Ngon(7, 60, Vec2{ 300, 500 })
			.toBuffer2D(Arg::center(300, 500), texture.size(), angle)
			.draw(texture);

		Shape2D::Plus(60, 30, Vec2{ 500, 500 })
			.toBuffer2D(Arg::center(500, 500), texture.size(), angle)
			.draw(texture);

		Shape2D::Heart(60, Vec2{ 700, 500 })
			.toBuffer2D(Arg::center(700, 500), texture.size(), angle)
			.draw(texture);
	}
}
Ryo SuzukiRyo Suzuki

ImageROI

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

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

void Main()
{
	Image image{ U"example/windmill.png" };
	DynamicTexture texture{ image };

	while (System::Update())
	{
		if (MouseL.down())
		{
			// Image 内の指定領域をガウスぼかし
			image(Cursor::Pos().movedBy(-20, -20), 40, 40)
				.gaussianBlur(25);
			
			texture.fill(image);
		}

		texture.draw();
	}
}
Ryo SuzukiRyo Suzuki

Image::warpAffine(), warpPerspective()

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

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

void Main()
{
	Window::Resize(1280, 720);

	const Image image1{ U"🐤"_emoji };
	const Image image2{ U"example/windmill.png" };

	const Texture texture1{ image1 };
	const Texture texture2{ image2 };
	
	const Mat3x2 mat = Mat3x2::Rotate(50_deg, image1.size() / 2.0);
	const Texture texture1t{ image1.warpAffine(mat) };

	const Quad q{ Vec2{ 0, 80 }, Vec2{ 400, 0 }, Vec2{ 400, 300 }, Vec2{ 0, 220 } };
	const Texture texture2t{ image2.warpPerspective(q) };

	while (System::Update())
	{
		texture1.draw(0, 0);
		texture2.draw(0, 200);

		texture1t.draw(600, 0).drawFrame(1, 0);
		texture2t.draw(600, 200).drawFrame(1, 0);
	}
}
Ryo SuzukiRyo Suzuki

Mat3x3::Homography()

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

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

struct Homography
{
	Float4 m1;
	Float4 m2;
	Float4 m3;
};

// チェッカーパターンの Image を作る
Image MakeCheckerPattern()
{
	Image image{ 1280, 720 , Palette::White };
	for (auto p : step(image.size() / Size{ 40, 40 }))
	{
		if (IsEven(p.x + p.y))
		{
			Rect{ p * 40, 40 }.overwrite(image, Color{ 40 });
		}
	}
	return image;
}

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

	const Texture texture{ U"example/bay.jpg", TextureDesc::Mipped };
	const Texture checker{ MakeCheckerPattern(), TextureDesc::Mipped };

	constexpr double circleR = 12.0;
	const VertexShader vs = HLSL{ U"example/shader/hlsl/homography.hlsl", U"VS" }
		| GLSL{ U"example/shader/glsl/homography.vert", {{ U"VSConstants2D", 0 }, { U"VSHomography", 1} } };
	const PixelShader ps = HLSL{ U"example/shader/hlsl/homography.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/homography.frag", {{ U"PSConstants2D", 0 }, { U"PSHomography", 1} } };

	if (!vs || !ps)
	{
		return;
	}

	ConstantBuffer<Homography> vsHomography;
	ConstantBuffer<Homography> psHomography;

	Quad quad{ Vec2{100, 300}, Vec2{500, 300}, Vec2{500, 600}, Vec2{100, 600} };
	Optional<size_t> grabbedIndex;

	bool homography = true;

	while (System::Update())
	{
		SimpleGUI::CheckBox(homography, U"Homography", Vec2{ 40, 40 });

		if (homography)
		{
			ScopedCustomShader2D shader{ vs, ps };
			ScopedRenderStates2D sampler{ SamplerState::ClampAniso };

			{
				const Mat3x3 mat = Mat3x3::Homography(quad.movedBy(580, 0));
				vsHomography = { Float4{ mat._11_12_13, 0 }, Float4{ mat._21_22_23, 0 }, Float4{ mat._31_32_33, 0 } };
				Graphics2D::SetConstantBuffer(ShaderStage::Vertex, 1, vsHomography);

				const Mat3x3 inv = mat.inverse();
				psHomography = { Float4{ inv._11_12_13, 0 }, Float4{ inv._21_22_23, 0 }, Float4{ inv._31_32_33, 0 } };
				Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, psHomography);

				// 1x1 の Rect に貼り付けて描くと、適切にホモグラフィ変換される
				Rect{ 1 }(checker).draw();
			}

			{
				const Mat3x3 mat = Mat3x3::Homography(quad);
				vsHomography = { Float4{ mat._11_12_13, 0 }, Float4{ mat._21_22_23, 0 }, Float4{ mat._31_32_33, 0 } };
				Graphics2D::SetConstantBuffer(ShaderStage::Vertex, 1, vsHomography);

				const Mat3x3 inv = mat.inverse();
				psHomography = { Float4{ inv._11_12_13, 0 }, Float4{ inv._21_22_23, 0 }, Float4{ inv._31_32_33, 0 } };
				Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, psHomography);

				// 1x1 の Rect に貼り付けて描くと、適切にホモグラフィ変換される
				Rect{ 1 }(texture).draw();
			}
		}
		else
		{
			quad.movedBy(580, 0)(checker).draw();
			quad(texture).draw();
		}

		if (grabbedIndex)
		{
			if (not MouseL.pressed())
			{
				grabbedIndex.reset();
			}
			else
			{
				quad.p(*grabbedIndex).moveBy(Cursor::DeltaF());
			}
		}
		else
		{
			for (auto i : step(4))
			{
				const Circle circle = quad.p(i).asCircle(circleR);

				if (circle.mouseOver())
				{
					Cursor::RequestStyle(CursorStyle::Hand);
				}

				if (circle.leftClicked())
				{
					grabbedIndex = i;
					break;
				}
			}
		}

		for (auto i : step(4))
		{
			quad.p(i).asCircle(circleR).draw(ColorF{ 1.0, 0.3, 0.3, 0.5 });
		}
	}
}
Ryo SuzukiRyo Suzuki

VideoReader

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

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

void ShowStatus(const VideoReader& video)
{
	Print << U"file: " << FileSystem::FileName(video.path());
	Print << U"size: " << video.getSize();
	Print << U"fps: " << video.getFPS();
	Print << U"time: {:.2f}s/{:.2f}s"_fmt(video.getPosSec(), video.getLengthSec());
	Print << U"progress: {:.0f} %"_fmt(video.getProgress() * 100.0);
	Print << U"frame: {}/{}"_fmt(video.getCurrentFrameIndex(), video.getFrameCount());
	Print << U"reachedEnd: " << video.reachedEnd();
}

void Main()
{
	Window::Resize(1280, 720);

	VideoReader video{ U"example/video/river.mp4" };

	if (!video)
	{
		throw Error{ U"Failed to load the video" };
	}

	Image frameImage;
	video.readFrame(frameImage);
	DynamicTexture frameTexture{ frameImage };

	const double frameDeltaSec = video.getFrameDeltaSec();
	double frameTimeSec = 0.0;
	bool playing = true;

	while (System::Update())
	{
		ClearPrint();
		ShowStatus(video);

		if (playing)
		{
			frameTimeSec += Scene::DeltaTime();
		}

		if (frameDeltaSec <= frameTimeSec)
		{
			video.readFrame(frameImage);
			frameTexture.fill(frameImage);
			frameTimeSec -= frameDeltaSec;
		}

		frameTexture.draw();

		if (SimpleGUI::Button(U"Reset", Vec2{ 40, 640 }))
		{
			video.setCurrentFrameIndex(0);
			video.readFrame(frameImage);
			frameTexture.fill(frameImage);
			frameTimeSec = 0.0;
		}

		if (SimpleGUI::Button((playing ? U"■" : U"▶"), Vec2{ 160, 640 }))
		{
			playing = !playing;
			frameTimeSec = 0.0;
		}
	}
}

Ryo SuzukiRyo Suzuki

GrabCut

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

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

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.8, 1.0, 0.9 });
	
	const Image image{ U"example/windmill.png" };
	const Texture texture{ image };

	GrabCut grabcut{ image };
	Image mask{ image.size(), Color{0, 0} };
	Image background{ image.size(), Palette::Black };
	Image foreground{ image.size(), Palette::Black };
	Image inpaint;
	DynamicTexture maskTexture{ mask };
	Grid<GrabCutClass> result;
	DynamicTexture classTexture;
	DynamicTexture backgroundTexture{ background };
	DynamicTexture foregroundTexture{ foreground };
	DynamicTexture inpaintTexture{ foreground };

	constexpr Color BackgroundColor{ 0, 0, 255 };
	constexpr Color ForegroundColor{ 250, 100, 50 };

	while (System::Update())
	{
		if (!classTexture || MouseL.up() || MouseR.up())
		{
			grabcut.update(mask, ForegroundColor, BackgroundColor);
			grabcut.getResult(result);
			classTexture.fill(Image(result, [](GrabCutClass c) { return Color(80 * FromEnum(c)); }));

			for (auto p : step(image.size()))
			{
				const bool isBackground = (GrabCutClass::PossibleBackground <= result[p]);

				if (isBackground)
				{
					background[p] = image[p];
					foreground[p] = Color{ 0,0 };
				}
				else
				{
					foreground[p] = image[p];
					background[p] = Color{ 0,0 };
				}
			}

			ImageProcessing::Inpaint(background, background, Color{ 0, 0 }, inpaint);
			inpaint.gaussianBlur(3);

			foregroundTexture.fill(foreground);
			backgroundTexture.fill(background);
			inpaintTexture.fill(inpaint);
		}

		if (MouseL.pressed())
		{
			const Point from = MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos();
			const Point to = Cursor::Pos();
			Line{ from, to }.overwrite(mask, 4, ForegroundColor, Antialiased::No);
			maskTexture.fill(mask);
		}
		else if (MouseR.pressed())
		{
			const Point from = MouseR.down() ? Cursor::Pos() : Cursor::PreviousPos();
			const Point to = Cursor::Pos();
			Line{ from, to }.overwrite(mask, 4, BackgroundColor, Antialiased::No);
			maskTexture.fill(mask);
		}

		texture.draw();
		maskTexture.draw();
		classTexture.draw(600, 0);

		backgroundTexture.scaled(0.7).regionAt(200, 520).draw(ColorF{ 0 });
		backgroundTexture.scaled(0.7).drawAt(200, 520);

		foregroundTexture.scaled(0.7).regionAt(1080, 520).draw(ColorF{ 0 });
		foregroundTexture.scaled(0.7).drawAt(1080, 520);

		inpaintTexture.drawAt(640, 520);
		{
			Transformer2D t{ Mat3x2::Scale(1.1, Vec2{640, 520}.movedBy(0, image.height() / 2)).translated((Scene::Center() - Cursor::Pos()) * 0.04) };
			foregroundTexture.drawAt(640, 520);
		}
	}
}
Ryo SuzukiRyo Suzuki

VideoTexture

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

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

void Main()
{
	const VideoTexture videoTexture{ U"example/video/river.mp4", Loop::Yes };

	while (System::Update())
	{
		// 動画の時間を進める (デフォルトでは Scece::DeltaTime() 秒)
		videoTexture.advance();

		videoTexture(400, 400, 300, 200)
			.draw();

		videoTexture
			.scaled(0.5)
			.rotated(Scene::Time() * 30_deg)
			.drawAt(Cursor::Pos());
	}
}
Ryo SuzukiRyo Suzuki

動画編集

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

void Main()
{
	VideoReader reader{ U"example/video/river.mp4" };
	VideoWriter writer{ U"output.mp4", reader.getSize(), reader.getFPS() };
	Image frame;

	for (size_t i = 0; i < reader.getFrameCount(); ++i)
	{
		Console << U"{} フレーム目"_fmt(i);
		reader.readFrame(frame);
		writer.writeFrame(frame.grayscale());
	}
}
Ryo SuzukiRyo Suzuki

2D 物理演算 API 刷新

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

void Main()
{
	// ウィンドウを 1280x720 にリサイズ
	Window::Resize(1280, 720);

	// 背景色を設定
	Scene::SetBackground(ColorF{ 0.4, 0.7, 1.0 });

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

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

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

	// [_] 地面
	const P2Body ground = world.createLine(P2Static, Vec2{ 0, 0 }, Line{ -1600, 0, 1600, 0 });

	// [■] 箱 (Sleep させておく)
	Array<P2Body> boxes;
	{
		for (auto y : Range(0, 12))
		{
			for (auto x : Range(0, 20))
			{
				boxes << world.createRect(P2Dynamic, Vec2{ x * 50, -50 - y * 100 },
					SizeF{ 50, 100 }, P2Material{ .density = 0.02, .restitution = 0.0, .friction = 1.0 })
					.setAwake(false);
			}
		}
	}

	// 振り子の軸の座標
	constexpr Vec2 pivotPos{ 0, -2400 };

	// チェーンを構成するリンク 1 つの長さ
	constexpr double linkLength = 100.0;

	// チェーンを構成するリンクの数
	constexpr int32 linkCount = 16;

	// チェーンの長さ
	constexpr double chainLength = (linkLength * linkCount);

	// 鉄球の半径
	constexpr double ballR = 200;

	// 鉄球の初期座標
	constexpr Vec2 ballCenter = pivotPos.movedBy(-chainLength - ballR, 0);

	// [●] 鉄球
	const P2Body ball = world.createCircle(P2BodyType::Dynamic, ballCenter, ballR,
		P2Material{ .density = 0.5, .restitution = 0.0, .friction = 1.0 });

	// [ ] 振り子の軸(実体がないプレースホルダー)
	const P2Body pivot = world.createPlaceholder(P2BodyType::Static, pivotPos);

	// [-] チェーンを構成するリンク
	Array<P2Body> links;

	// リンクどうしやリンクと鉄球をつなぐジョイント
	Array<P2PivotJoint> joints;
	{
		for (auto i : step(linkCount))
		{
			// リンクの長方形(隣接するリンクと重なるよう少し大きめに)
			const RectF rect{ Arg::rightCenter = pivotPos.movedBy(i * -linkLength, 0), linkLength * 1.2, 20 };

			// categoryBits を 0 にすることで、箱など他の物体と干渉しないようにする
			links << world.createRect(P2Dynamic, rect.center(), rect.size,
				P2Material{ .density = 0.1, .restitution = 0.0, .friction = 1.0 }, P2Filter{ .categoryBits = 0 });

			if (i == 0)
			{
				// 振り子の軸と最初のリンクをつなぐジョイント
				joints << world.createPivotJoint(pivot, links.back(), rect.rightCenter().movedBy(-linkLength * 0.1, 0));
			}
			else
			{
				// 新しいリンクと、一つ前のリンクをつなぐジョイント
				joints << world.createPivotJoint(links[links.size() - 2], links.back(), rect.rightCenter().movedBy(-linkLength * 0.1, 0));
			}
		}

		// 最後のリンクと鉄球をつなぐジョイント
		joints << world.createPivotJoint(links.back(), ball, pivotPos.movedBy(-chainLength, 0));
	}

	// [/] ストッパー
	P2Body stopper = world.createLine(P2Static, ballCenter.movedBy(0, 200), Line{ -400, 200, 400, 0 });

	// 2D カメラ
	Camera2D camera{ Vec2{ 0, -1200 }, 0.25 };

	while (System::Update())
	{
		for (accumulatorSec += Scene::DeltaTime(); stepSec <= accumulatorSec; accumulatorSec -= stepSec)
		{
			// 2D 物理演算のワールドを更新
			world.update(stepSec);

			// 落下した box は削除
			boxes.remove_if([](const P2Body& body) { return (2000 < body.getPos().y); });
		}

		// 2D カメラの更新
		camera.update();
		{
			// 2D カメラから Transformer2D を作成
			const auto t = camera.createTransformer();

			// 地面を描く
			ground.draw(ColorF{ 0.0, 0.5, 0.0 });

			// チェーンを描く
			for (const auto& link : links)
			{
				link.draw(ColorF{ 0.25 });
			}

			// 箱を描く
			for (const auto& box : boxes)
			{
				box.draw(ColorF{ 0.6, 0.4, 0.2 });
			}

			// ストッパーを描く
			stopper.draw(ColorF{ 0.25 });

			// 鉄球を描く
			ball.draw(ColorF{ 0.25 });
		}

		// ストッパーを無くす
		if (stopper && SimpleGUI::Button(U"Go", Vec2(1100, 20)))
		{
			// ストッパーを破棄
			stopper.release();
		}

		// 2D カメラの操作を描画
		camera.draw(Palette::Orange);
	}
}
Ryo SuzukiRyo Suzuki

P2WheelJoint

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

void Main()
{
	// ウィンドウを 1280x720 にリサイズ
	Window::Resize(1280, 720);

	// 背景色を設定
	Scene::SetBackground(ColorF{ 0.4, 0.7, 1.0 });

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

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

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

	// [_] 地面
	Array<P2Body> floors;
	{
		floors << world.createLine(P2Static, Vec2{ 0, 0 }, Line{ -1600, 0, 1600, 0 });

		for (auto i : Range(1, 5))
		{
			if (IsEven(i))
			{
				floors << world.createLine(P2Static, Vec2{ 0, 0 }, Line{ 0, -i * 200, 1600, -i * 200 - 300 });
			}
			else
			{
				floors << world.createLine(P2Static, Vec2{ 0, 0 }, Line{ -1600,  -i * 200 - 300, 0, -i * 200 });
			}
		}
	}

	// [🚙] 車
	const P2Body carBody = world.createRect(P2Dynamic, Vec2{ -1500, -1450 }, SizeF{ 200, 40 });
	const P2Body wheelL = world.createCircle(P2Dynamic, Vec2{ -1550, -1430 }, 30);
	const P2Body wheelR = world.createCircle(P2Dynamic, Vec2{ -1450, -1430 }, 30);
	const P2WheelJoint wheelJointL = world.createWheelJoint(carBody, wheelL, wheelL.getPos(), Vec2{ 0, -1 })
		.setLinearStiffness(4.0, 0.7)
		.setLimits(-5, 5).setLimitsEnabled(true);
	const P2WheelJoint wheelJointR = world.createWheelJoint(carBody, wheelR, wheelR.getPos(), Vec2{ 0, -1 })
		.setLinearStiffness(4.0, 0.7)
		.setLimits(-5, 5).setLimitsEnabled(true);

	// マウスジョイント
	P2MouseJoint mouseJoint;

	// 2D カメラ
	Camera2D camera{ Vec2{ 0, -1200 }, 0.25 };

	while (System::Update())
	{
		for (accumulatorSec += Scene::DeltaTime(); stepSec <= accumulatorSec; accumulatorSec -= stepSec)
		{
			world.update(stepSec);
		}

		// 2D カメラの更新
		camera.update();
		{
			// 2D カメラから Transformer2D を作成
			const auto t = camera.createTransformer();

			if (MouseL.down())
			{
				mouseJoint = world.createMouseJoint(carBody, Cursor::PosF())
					.setMaxForce(carBody.getMass() * 5000.0)
					.setLinearStiffness(2.0, 0.7);
			}
			else if (MouseL.pressed())
			{
				mouseJoint.setTargetPos(Cursor::PosF());
			}
			else if (MouseL.up())
			{
				mouseJoint.release();
			}

			// 地面を描く
			for (const auto& floor : floors)
			{
				floor.draw(ColorF{ 0.0, 0.5, 0.0 });
			}

			carBody.draw(Palette::Gray);
			wheelL.draw(Palette::Gray).drawWireframe(1, Palette::Yellow);
			wheelR.draw(Palette::Gray).drawWireframe(1, Palette::Yellow);

			mouseJoint.draw();
			wheelJointL.draw();
			wheelJointR.draw();
		}

		// 2D カメラの操作を描画
		camera.draw(Palette::Orange);
	}
}
Ryo SuzukiRyo Suzuki

P2WheelJoint (2)

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

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

void Main()
{
	// ウィンドウを 1280x720 にリサイズ
	Window::Resize(1280, 720);

	// 背景色を設定
	Scene::SetBackground(ColorF{ 0.2 });

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

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

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

	const P2Body rail = world.createLineString(P2Static, Vec2{ 0, -400 }, { Vec2{-400, -40}, Vec2{-400, 0}, Vec2{400, 0}, {Vec2{400, -40}} });
	const P2Body wheel = world.createCircle(P2Dynamic, Vec2{ 0, -420 }, 20);
	const P2Body car = world.createCircle(P2Dynamic, Vec2{ 0, -380 }, 10).setFixedRotation(true);	
	
	// ホイールジョイント
	const P2WheelJoint wheelJoint = world.createWheelJoint(car, wheel, wheel.getPos(), Vec2{ 0, 1 })
		.setLimitsEnabled(true);

	const P2Body box = world.createPolygon(P2Dynamic, Vec2{ 0, 0 }, LineString{ Vec2{-100, 0}, Vec2{-100, 100}, Vec2{100, 100}, {Vec2{100, 0}} }.calculateBuffer(5), P2Material{ .friction = 0.0 });
	
	// 距離ジョイント
	const P2DistanceJoint distanceJointL = world.createDistanceJoint(car, car.getPos(), box, Vec2{-100, 0}, 400);
	const P2DistanceJoint distanceJointR = world.createDistanceJoint(car, car.getPos(), box, Vec2{ 100, 0}, 400);

	Array<P2Body> balls;

	// マウスジョイント
	P2MouseJoint mouseJoint;

	// 2D カメラ
	Camera2D camera{ Vec2{ 0, -150 } };

	while (System::Update())
	{
		for (accumulatorSec += Scene::DeltaTime(); stepSec <= accumulatorSec; accumulatorSec -= stepSec)
		{
			world.update(stepSec);
		}

		// こぼれたボールの削除
		balls.remove_if([](const P2Body& b) { return (600 < b.getPos().y); });

		// 2D カメラの更新
		camera.update();
		{
			// 2D カメラから Transformer2D を作成
			const auto t = camera.createTransformer();

			// マウスジョイントによる干渉
			if (MouseL.down())
			{
				mouseJoint = world.createMouseJoint(box, Cursor::PosF())
					.setMaxForce(box.getMass() * 5000.0)
					.setLinearStiffness(2.0, 0.7);
			}
			else if (MouseL.pressed())
			{
				mouseJoint.setTargetPos(Cursor::PosF());
			}
			else if (MouseL.up())
			{
				mouseJoint.release();
			}

			if (KeySpace.pressed())
			{
				// ボールの追加
				balls << world.createCircle(P2Dynamic, Cursor::PosF(), Random(2.0, 4.0), P2Material{ .density = 0.001, .restitution = 0.5, .friction = 0.0 });
			}

			rail.draw(Palette::Gray);
			wheel.draw(Palette::Gray).drawWireframe(1, Palette::Yellow);
			car.draw(ColorF{ 0.3, 0.8, 0.5 });
			box.draw(ColorF{ 0.3, 0.8, 0.5 });

			for (const auto& ball : balls)
			{
				ball.draw(Palette::Skyblue);
			}

			distanceJointL.draw();
			distanceJointR.draw();

			mouseJoint.draw();
		}

		// 2D カメラの操作を描画
		camera.draw(Palette::Orange);
	}
}
Ryo SuzukiRyo Suzuki

絵文字タワー (v0.6 版)

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

void Main()
{
	// ウィンドウを 1280x720 にリサイズ
	Window::Resize(1280, 720);

	// 背景色を設定
	Scene::SetBackground(ColorF{ 0.2, 0.6, 1.0 });

	// 登場する絵文字
	const Array<String> emojis = { U"🐘", U"🐧", U"🐐", U"🐤" };
	Array<MultiPolygon> polygons;
	Array<Texture> textures;
	for (const auto& emoji : emojis)
	{
		// 絵文字の画像から形状情報を作成
		polygons << Emoji::CreateImage(emoji).alphaToPolygonsCentered().simplified(2.0);

		// 絵文字の画像からテクスチャを作成
		textures << Texture{ Emoji{ emoji } };
	}

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

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

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

	// [_] 地面
	const P2Body ground = world.createLine(P2Static, Vec2{ 0, 0 }, Line{ -300, 0, 300, 0 });
	
	// 動物の物体
	Array<P2Body> bodies;

	// 物体の ID と絵文字のインデックスの対応テーブル
	HashTable<P2BodyID, size_t> table;

	// 絵文字のインデックス
	size_t index = Random(polygons.size() - 1);

	// 2D カメラ
	Camera2D camera{ Vec2{ 0, -200 } };

	while (System::Update())
	{
		for (accumulatorSec += Scene::DeltaTime(); stepSec <= accumulatorSec; accumulatorSec -= stepSec)
		{
			// 2D 物理演算のワールドを更新
			world.update(stepSec);
		}

		// 地面より下に落ちた物体は削除
		for (auto it = bodies.begin(); it != bodies.end();)
		{
			if (100 < it->getPos().y)
			{
				// 対応テーブルからも削除
				table.erase(it->id());

				it = bodies.erase(it);
			}
			else
			{
				++it;
			}
		}

		// 2D カメラの更新
		camera.update();
		{
			// 2D カメラから Transformer2D を作成
			const auto t = camera.createTransformer();

			// 左クリックされたら
			if (MouseL.down())
			{
				// ボディを追加
				bodies << world.createPolygons(P2Dynamic, Cursor::PosF(), polygons[index], P2Material{ 0.1, 0.0, 1.0 });

				// ボディ ID と絵文字のインデックスの組を対応テーブルに追加
				table.emplace(bodies.back().id(), std::exchange(index, Random(polygons.size() - 1)));
			}

			// すべてのボディを描画
			for (const auto& body : bodies)
			{
				textures[table[body.id()]].rotated(body.getAngle()).drawAt(body.getPos());
			}

			// 地面を描画
			ground.draw(Palette::Green);

			// 現在操作できる絵文字を描画
			textures[index].drawAt(Cursor::PosF(), AlphaF(0.5 + Periodic::Sine0_1(1s) * 0.5));
		}

		// 2D カメラの操作を描画
		camera.draw(Palette::Orange);
	}
}
Ryo SuzukiRyo Suzuki

Sketch to P2Body (v0.6 版)

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

void Main()
{
	// ウィンドウを 1280x720 にリサイズ
	Window::Resize(1280, 720);

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

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

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

	// [_] 地面
	const P2Body ground = world.createLine(P2Static, Vec2{ 0, 0 }, Line{ -600, 0, 600, 0 });
	
	// 物体
	Array<P2Body> bodies;

	// 2D カメラ
	Camera2D camera{ Vec2{ 0, -300 } };

	LineString points;

	while (System::Update())
	{
		for (accumulatorSec += Scene::DeltaTime(); stepSec <= accumulatorSec; accumulatorSec -= stepSec)
		{
			// 2D 物理演算のワールドを更新
			world.update(stepSec);
		}

		// 地面より下に落ちた物体は削除
		bodies.remove_if([](const P2Body& b) { return (100 < b.getPos().y); });

		// 2D カメラの更新
		camera.update();
		{
			// 2D カメラから Transformer2D を作成
			const auto t = camera.createTransformer();

			// 左クリックされたら
			if (MouseL.down())
			{
				points << Cursor::PosF();
			}
			else if (MouseL.pressed() && !Cursor::DeltaF().isZero())
			{
				points << Cursor::PosF();
			}
			else if (MouseL.up())
			{
				points = points.simplified(2.0);

				if (const Polygon polygon = Polygon::CorrectOne(points))
				{
					const Vec2 pos = polygon.centroid();

					bodies << world.createPolygon(P2Dynamic, pos, polygon.movedBy(-pos));
				}

				points.clear();
			}

			// すべてのボディを描画
			for (const auto& body : bodies)
			{
				body.draw(HSV{ body.id() * 10.0 });
			}

			// 地面を描画
			ground.draw(Palette::Skyblue);

			points.draw(3);
		}

		// 2D カメラの操作を描画
		camera.draw(Palette::Orange);
	}
}
Ryo SuzukiRyo Suzuki

子プロセス作成 (v0.6 版)

# include <Siv3D.hpp>

void Main()
{
# if SIV3D_PLATFORM(WINDOWS)

    // 子プロセスを作成
    ChildProcess child{ U"C:/Windows/System32/notepad.exe" };

# elif SIV3D_PLATFORM(MACOS)

    // 子プロセスを作成
    ChildProcess child{ U"/System/Applications/Calculator.app/Contents/MacOS/Calculator" };

# elif SIV3D_PLATFORM(LINUX)

    // 子プロセスを作成
    ChildProcess child{ U"/usr/bin/firefox", U"www.mozilla.org" };

# endif

    if (!child)
    {
        throw Error{ U"Failed to create a process" };
    }

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

        // プロセスが実行中かを取得
        Print << child.isRunning();

        // プロセスが終了した場合、その終了コード
        Print << child.getExitCode();

        if (child.isRunning())
        {
            if (SimpleGUI::Button(U"Terminate", Vec2{ 600, 20 }))
            {
                // プロセスを強制終了
                child.terminate();
            }
        }
    }
}
Ryo SuzukiRyo Suzuki

プロセスとの標準入出力のパイプライン処理 (v0.6 版)

# include <Siv3D.hpp>

void Main()
{
# if SIV3D_PLATFORM(WINDOWS)

    // 子プロセスを作成(パイプライン処理)
    ChildProcess child{ U"Console.exe", Pipe::StdInOut };

# else

    // 子プロセスを作成(パイプライン処理)
    ChildProcess child{ U"Console", Pipe::StdInOut };

# endif

    if (!child)
    {
        throw Error{ U"Failed to create a process" };
    }

    child.ostream() << 10 << std::endl;
    child.ostream() << 20 << std::endl;

    int32 result;
    child.istream() >> result;
    Print << U"result: " << result;

    while (System::Update())
    {

    }
}

子プロセスのプログラム

# include <iostream>

int main()
{
    int a, b;
    std::cin >> a >> b;
    std::cout << (a + b) << std::endl;
}
Ryo SuzukiRyo Suzuki

XInput (v0.6 版)

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

Polygon MakeGamePadPolygon()
{
	Polygon polygon = Ellipse{ 400, 480, 300, 440 }.asPolygon(64);
	polygon = Geometry2D::Subtract(polygon, Ellipse{ 400, 40, 220, 120 }.asPolygon(48)).front();
	polygon = Geometry2D::Subtract(polygon, Circle{ 400, 660, 240 }.asPolygon(48)).front();
	polygon = Geometry2D::Subtract(polygon, Rect{ 0, 540, 800, 60 }.asPolygon()).front();
	return polygon;
}

void Main()
{
	constexpr ColorF backgroundColor{ 0.6, 0.8, 0.7 };
	Scene::SetBackground(backgroundColor);

	constexpr Ellipse buttonLB{ 210, 150, 50, 24 };
	constexpr Ellipse buttonRB{ 590, 150, 50, 24 };
	const Polygon gamepadPolygon = MakeGamePadPolygon();
	constexpr Circle logo{ 400, 250, 25 };
	constexpr RectF leftTrigger{ 210, 16, 40, 100 };
	constexpr RectF rightTrigger{ 550, 16, 40, 100 };
	constexpr Circle leftThumb{ 230, 250, 35 };
	constexpr Circle rightThumb{ 480, 350, 35 };
	constexpr Circle dPad{ 320, 350, 40 };
	constexpr Circle buttonA{ 570, 300, 20 };
	constexpr Circle buttonB{ 620, 250, 20 };
	constexpr Circle buttonX{ 520, 250, 20 };
	constexpr Circle buttonY{ 570, 200, 20 };
	constexpr Circle buttonView{ 330, 250, 15 };
	constexpr Circle buttonMenu{ 470, 250, 15 };

	// プレイヤーインデックス (0 - 3)
	size_t playerIndex = 0;
	const Array<String> options = Range(1, 4).map([](int32 i) {return U"{}P"_fmt(i); });

	// デッドゾーンを有効にするか
	bool enableDeadZone = false;

	// 振動 (0.0 - 1.0)
	XInputVibration vibration;

	while (System::Update())
	{
		// 指定したプレイヤーインデックスの XInput コントローラを取得
		auto controller = XInput(playerIndex);

		// デッドゾーン
		if (enableDeadZone)
		{
			// それぞれデフォルト値を設定
			controller.setLeftTriggerDeadZone();
			controller.setRightTriggerDeadZone();
			controller.setLeftThumbDeadZone();
			controller.setRightThumbDeadZone();
		}
		else
		{
			// デッドゾーンを無効化
			controller.setLeftTriggerDeadZone(DeadZone{});
			controller.setRightTriggerDeadZone(DeadZone{});
			controller.setLeftThumbDeadZone(DeadZone{});
			controller.setRightThumbDeadZone(DeadZone{});
		}

		// 振動
		controller.setVibration(vibration);

		// L ボタン、R ボタン
		{
			buttonLB.draw(ColorF{ controller.buttonLB.pressed() ? 1.0 : 0.7 });
			buttonRB.draw(ColorF{ controller.buttonRB.pressed() ? 1.0 : 0.7 });
		}

		// 本体
		gamepadPolygon.draw(ColorF{ 0.9 });

		// Xbox ボタン
		{
			if (controller.isConnected())
			{
				Circle{ logo.center, 32 }
				.drawPie((-0.5_pi + 0.5_pi * controller.playerIndex), 0.5_pi, ColorF{ 0.6, 0.9, 0.3 });
			}

			logo.draw(ColorF{ 0.6 });
		}

		// 左トリガー
		{
			leftTrigger.draw(AlphaF(0.25));
			leftTrigger.stretched((controller.leftTrigger - 1.0) * leftTrigger.h, 0, 0, 0).draw();
		}

		// 右トリガー
		{
			rightTrigger.draw(AlphaF(0.25));
			rightTrigger.stretched((controller.rightTrigger - 1.0) * rightTrigger.h, 0, 0, 0).draw();
		}

		// 左スティック
		{
			leftThumb.draw(ColorF{ controller.buttonLThumb.pressed() ? 0.85 : 0.5 });
			Circle{ leftThumb.center + 25 * Vec2{ controller.leftThumbX, -controller.leftThumbY }, 20 }.draw();
		}

		// 右スティック
		{
			rightThumb.draw(ColorF{ controller.buttonRThumb.pressed() ? 0.85 : 0.5 });
			Circle{ rightThumb.center + 25 * Vec2{ controller.rightThumbX, -controller.rightThumbY }, 20 }.draw();
		}

		// 方向パッド
		{
			dPad.draw(ColorF{ 0.75 });
			Shape2D::Plus(dPad.r * 0.9, 25, dPad.center).draw(ColorF{ 0.5 });

			const Vec2 direction{
				controller.buttonRight.pressed() - controller.buttonLeft.pressed(),
				controller.buttonDown.pressed() - controller.buttonUp.pressed() };

			if (!direction.isZero())
			{
				Circle{ dPad.center + direction.withLength(25), 15 }.draw();
			}
		}

		// A, B, X, Y ボタン
		{
			buttonA.draw(ColorF{ 0.0, 1.0, 0.3, controller.buttonA.pressed() ? 1.0 : 0.3 });
			buttonB.draw(ColorF{ 1.0, 0.0, 0.3, controller.buttonB.pressed() ? 1.0 : 0.3 });
			buttonX.draw(ColorF{ 0.0, 0.3, 1.0, controller.buttonX.pressed() ? 1.0 : 0.3 });
			buttonY.draw(ColorF{ 1.0, 0.5, 0.0, controller.buttonY.pressed() ? 1.0 : 0.3 });
		}

		// View (Back), Menu (Start) ボタン 
		{
			buttonView.draw(ColorF(controller.buttonView.pressed() ? 1.0 : 0.7));
			buttonMenu.draw(ColorF(controller.buttonMenu.pressed() ? 1.0 : 0.7));
		}

		SimpleGUI::RadioButtons(playerIndex, options, Vec2{ 20, 20 });
		SimpleGUI::CheckBox(enableDeadZone, U"DeadZone", Vec2{ 320, 20 });
		SimpleGUI::Slider(U"leftMotor", vibration.leftMotor, Vec2{ 280, 420 }, 120, 120);
		SimpleGUI::Slider(U"rightMotor", vibration.rightMotor, Vec2{ 280, 460 }, 120, 120);
	}
}
Ryo SuzukiRyo Suzuki

Gamepad (v0.6 版)

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

void Main()
{
	Window::Resize(800, 800);

	const Array<String> indices = Range(1, Gamepad.MaxPlayerCount).map(Format);
	
	// ゲームパッドのプレイヤーインデックス
	size_t playerIndex = 0;

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

		if (const auto gamepad = Gamepad(playerIndex)) // 接続されていたら
		{
			const auto& info = gamepad.getInfo();

			Print << U"{} (VID: {}, PID: {})"_fmt(info.name, info.vendorID, info.productID);

			for (auto [i, button] : Indexed(gamepad.buttons))
			{
				Print << U"button{}: {}"_fmt(i, button.pressed());
			}

			for (auto [i, axe] : Indexed(gamepad.axes))
			{
				Print << U"axe{}: {}"_fmt(i, axe);
			}

			Print << U"POV: " << gamepad.povD8();
		}

		SimpleGUI::RadioButtons(playerIndex, indices, Vec2{ 500, 20 });
	}
}
Ryo SuzukiRyo Suzuki

Joy-Con (v0.6 版)

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

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

    Vec2 left{ 640 - 100, 100 }, right{ 640 + 100, 100 };
    double angle = 0_deg;
    double scale = 400.0;
    bool covered = true;

    while (System::Update())
    {
        Circle{ Vec2{ 640 - 300, 450 }, scale / 2 }.drawFrame(scale * 0.1);
        Circle{ Vec2{ 640 + 300, 450 }, scale / 2 }.drawFrame(scale * 0.1);

        // Joy-Con (L) を取得
        if (const auto joy = JoyConL(0))
        {
            joy.drawAt(Vec2(640 - 300, 450), scale, -90_deg - angle, covered);

            if (auto d = joy.povD8())
            {
                left += Circular{ 4, *d * 45_deg };
            }

            if (joy.button2.down())
            {
                effect.add([center = left](double t) {
                    Circle{ center, 20 + t * 200 }.drawFrame(10, 0, AlphaF(1.0 - t));
                    return t < 1.0;
                    });
            }
        }

        // Joy-Con (R) を取得
        if (const auto joy = JoyConR(0))
        {
            joy.drawAt(Vec2{ 640 + 300, 450 }, scale, 90_deg + angle, covered);

            if (auto d = joy.povD8())
            {
                right += Circular{ 4, *d * 45_deg };
            }

            if (joy.button2.down())
            {
                effect.add([center = right](double t) {
                    Circle{ center, 20 + t * 200 }.drawFrame(10, 0, AlphaF(1.0 - t));
                    return t < 1.0;
                    });
            }
        }

        Circle{ left, 30 }.draw(ColorF{ 0.0, 0.75, 0.9 });
        Circle{ right, 30 }.draw(ColorF{ 1.0, 0.4, 0.3 });
        effect.update();

        SimpleGUI::Slider(U"Rotation: ", angle, -180_deg, 180_deg, Vec2{ 20, 20 }, 120, 200);
        SimpleGUI::Slider(U"Scale: ", scale, 100.0, 600.0, Vec2{ 20, 60 }, 120, 200);
        SimpleGUI::CheckBox(covered, U"Covered", Vec2{ 20, 100 });
    }
}
Ryo SuzukiRyo Suzuki

Webcam (v0.6 版)

メインスレッドで作成する場合

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

void Main()
{
	Window::Resize(1280, 720);

	Webcam webcam{ 0, Size{ 1280, 720 }, StartImmediately::Yes };

	DynamicTexture texture;

	while (System::Update())
	{
		// macOS では、ユーザがカメラ使用の権限を許可しないと Webcam の作成に失敗する。再試行の手段を用意する
	# if SIV3D_PLATFORM(MACOS)
		if (!webcam)
		{
			if (SimpleGUI::Button(U"Retry", Vec2{ 20, 20 }))
			{
				webcam = Webcam{ 0, Size{ 1280, 720 }, StartImmediately::Yes };
			}
		}
	# endif
		
		if (webcam.hasNewFrame())
		{
			webcam.getFrame(texture);
		}

		if (texture)
		{
			texture.draw();
		}
	}
}

別スレッドで作成する場合

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

void Main()
{
	Window::Resize(1280, 720);

	AsyncTask<Webcam> task{ []() { return Webcam{ 0, Size{ 1280, 720 }, StartImmediately::Yes }; } };
	Webcam webcam;
	DynamicTexture texture;

	while (System::Update())
	{
		// macOS では、ユーザがカメラ使用の権限を許可しないと Webcam の作成に失敗する。再試行の手段を用意する
	# if SIV3D_PLATFORM(MACOS)
		if (!webcam && !task.valid())
		{
			if (SimpleGUI::Button(U"Retry", Vec2{ 20, 20 }))
			{
				task = AsyncTask{ []() { return Webcam{ 0, Size{ 1280, 720 }, StartImmediately::Yes }; } };
			}
		}
	# endif
		
		if (task.isReady())
		{
			webcam = task.get();

			if (webcam)
			{
				Print << webcam.getResolution();
			}
		}

		if (webcam.hasNewFrame())
		{
			webcam.getFrame(texture);
		}

		// Webcam 作成待機中は円を表示
		if (!webcam)
		{
			Circle{ Scene::Center(), 40 }
				.drawArc(Scene::Time() * 180_deg, 300_deg, 5, 5);
		}

		if (texture)
		{
			texture.draw();
		}
	}
}
Ryo SuzukiRyo Suzuki

QRScanner (v0.6 版)

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

void Main()
{
	// Version 40 の QR コードの場合 1920x1080 が必要かもしれない。
	constexpr Size cameraResolution{ 1280, 720 };

	Window::Resize(cameraResolution);

	AsyncTask<Webcam> task{ [=]() { return Webcam{ 0, cameraResolution, StartImmediately::Yes }; } };
	Webcam webcam;

	Image image;
	DynamicTexture texture;
	QRScanner qrScanner;
	Array<QRContent> contents;

	while (System::Update())
	{
		// macOS では、ユーザがカメラ使用の権限を許可しないと Webcam の作成に失敗する。再試行の手段を用意する
	# if SIV3D_PLATFORM(MACOS)
		if (!webcam && !task.valid())
		{
			if (SimpleGUI::Button(U"Retry", Vec2{ 20, 20 }))
			{
				task = AsyncTask{ [=]() { return Webcam{ 0, cameraResolution, StartImmediately::Yes }; } };
			}
		}
	# endif

		if (task.isReady())
		{
			webcam = task.get();
		}

		if (webcam.hasNewFrame())
		{
			webcam.getFrame(image);

			texture.fill(image);

			contents = qrScanner.scan(image);
		}

		// Webcam 作成待機中は円を表示
		if (!webcam)
		{
			Circle{ Scene::Center(), 40 }
				.drawArc(Scene::Time() * 180_deg, 300_deg, 5, 5);
		}

		if (texture)
		{
			texture.draw();
		}

		for (const auto& content : contents)
		{
			content.quad.drawFrame(4, Palette::Red);

			PutText(content.text, Arg::topLeft = content.quad.p0);
		}
	}
}
Ryo SuzukiRyo Suzuki

Glyph 単位での描画 (v0.6 版)

# include <Siv3D.hpp>

void Main()
{
	const FontMethod fontMethod = FontMethod::MSDF;
	const Font font{ fontMethod, 50, Typeface::Bold };
	const String text = U"The quick brown fox\njumps over the lazy dog.";

	while (System::Update())
	{
		constexpr Vec2 basePos{ 20, 20 };

		Vec2 penPos{ basePos };

		// 現在のフォント用のピクセルシェーダを適用
		ScopedCustomShader2D shader{ Font::GetPixelShader(fontMethod) };

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

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

				continue;
			}

			// 何文字目かに応じて色を変える
			const ColorF color = HSV{ i * 30 };

			// 文字のテクスチャをペンの位置に文字ごとのオフセットを加算して描画
			Vec2 pos = penPos + glyph.getOffset();
			
			if (fontMethod == FontMethod::Bitmap) // FontMethod::Bitmap の場合、整数座標にすると描画品質が良い
			{
				pos = Math::Round(pos);
			}

			glyph.texture.draw(pos, color);

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

DragDrop (v0.6 版)

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

void Main()
{
	bool acceptFiles = true;
	bool acceptText = false; // デフォルトではテキストのドロップは受け付けない

	while (System::Update())
	{
		// ファイルパスがドロップされた
		if (DragDrop::HasNewFilePaths())
		{
			// アイテムを取得
			for (const auto& dropped : DragDrop::GetDroppedFilePaths())
			{
				Print << U"path:";
				Print << dropped.path;
				Print << dropped.timeMillisec << U" " << dropped.pos;
			}
		}

		// テキストがドロップされた
		if (DragDrop::HasNewText())
		{
			// アイテムを取得
			for (const auto& dropped : DragDrop::GetDroppedText())
			{
				Print << U"text:";
				Print << dropped.text;
				Print << dropped.timeMillisec << U" " << dropped.pos;
			}
		}

		// 何かをドラッグ中
		if (const auto drag = DragDrop::DragOver())
		{
			const Vec2 pos = drag->cursorPos;

			Circle{ pos, 80 }.draw(Palette::Gray);
			
			// ドラッグしているアイテムの種類
			if (drag->itemType == DragItemType::FilePaths)
			{
				PutText(U"Files", pos.movedBy(80, 80));
			}
			else
			{
				PutText(U"Text", pos.movedBy(80, 80));
			}
		}

		if (SimpleGUI::CheckBox(acceptFiles, U"Accept files", Vec2{ 400, 20 }))
		{
			DragDrop::AcceptFilePaths(acceptFiles);
		}

		if (SimpleGUI::CheckBox(acceptText, U"Accept text", Vec2{ 400, 60 }))
		{
			DragDrop::AcceptText(acceptText);
		}
	}
}
Ryo SuzukiRyo Suzuki

GeoJSON

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

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

void Main()
{
	Window::Resize(1280, 720);

	const Array<MultiPolygon> countries = GeoJSONFeatureCollection{ JSON::Load(U"example/geojson/countries.geojson") }.getFeatures()
		.map([](const GeoJSONFeature& f) { return f.getGeometry().getPolygons(); });

	Camera2D camera{ Vec2{ 0, 0 }, 2.0, Camera2DParameters{.maxScale = 4096.0 } };
	Optional<size_t> selected;

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

		camera.update();
		{
			const auto transformer = camera.createTransformer();
			const double lineThickness = (1.0 / Graphics2D::GetMaxScaling());
			const RectF viewRect = camera.getRegion();

			Print << Cursor::PosF();
			Print << camera.getScale() << U"x";

			Rect{ Arg::center(0, 0), 360, 180 }.draw(ColorF{ 0.2, 0.6, 0.9 }); // 海
			{
				for (auto [i, country] : Indexed(countries))
				{
					// 画面外にある場合は描画をスキップ
					if (!country.computeBoundingRect().intersects(viewRect))
					{
						continue;
					}

					if (country.leftClicked())
					{
						selected = i;
					}

					country.draw((selected == i) ? ColorF{ 0.9, 0.8, 0.7 } : ColorF{ 0.93, 0.99, 0.96 });
					country.drawFrame(lineThickness, ColorF{ 0.25 });
				}
			}
		}
		camera.draw(Palette::Orange);
	}
}
Ryo SuzukiRyo Suzuki

Siv3D くんドット絵素材

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

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

void Main()
{
	Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });

	const Texture texture{ U"example/spritesheet/siv3d-kun-16.png" };
	constexpr int32 patterns[4] = { 1, 2, 1, 0 };

	while (System::Update())
	{
		ScopedRenderStates2D sampler{ SamplerState::ClampNearest };

		const uint64 t = Time::GetMillisec();
		const uint64 x = ((t / 2000 % 2) * 3);
		const uint64 y = (t / 4000 % 4);
		const uint64 n = (t / 250 % 4);

		Rect{ (patterns[n] + x) * 20 * 4, y * 28 * 4, 20 * 4, 28 * 4 }
			.draw(ColorF{ 0.3, 0.9, 0.8 });
	
		texture.scaled(4).draw();

		Rect{ 520, 60, 20 * 8 + 80, 28 * 8 + 80 }
			.draw(ColorF{ 0.5, 0.9, 0.5 });

		texture((patterns[n] + x) * 20, y * 28, 20, 28)
			.scaled(8).draw(560, 100);
	}
}
Ryo SuzukiRyo Suzuki

PerlinNoise (v0.6 版)

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

void Main()
{
	Window::Resize(1280, 720);

	Image image1{ 512, 512, Palette::White };
	Image image2{ 512, 512, Palette::White };
	DynamicTexture texture1{ image1 };
	DynamicTexture texture2{ image2 };

	PerlinNoise noise;
	size_t oct = 5;
	double persistence = 0.5;
	const Array<String> options = Range(1, 6).map(Format);

	while (System::Update())
	{
		SimpleGUI::RadioButtons(oct, options, Vec2{ 1040, 40 });

		SimpleGUI::Slider(U"{:.2f}"_fmt(persistence), persistence, Vec2{ 1040, 280 });

		if (SimpleGUI::Button(U"Generate", Vec2{ 1040, 320 }))
		{
			noise.reseed(RandomUint64());
			const int32 octaves = static_cast<int32>(oct + 1);

			for (auto p : step(image1.size()))
			{
				image1[p] = ColorF{ noise.normalizedOctave2D0_1(p / 128.0, octaves, persistence) };
			}
			for (auto p : step(image2.size()))
			{
				image2[p] = ColorF{ noise.octave2D0_1(p / 128.0, octaves, persistence) };
			}
			texture1.fill(image1);
			texture2.fill(image2);
		}

		texture1.draw();
		texture2.draw(512, 0);
	}
}
Ryo SuzukiRyo Suzuki

シリアライズ・デシリアライズ (v0.6 版)

# include <Siv3D.hpp>

// ユーザデータとゲームのスコアを記録する構造体
struct GameScore
{
	String name;

	int32 id;

	int32 score;

	// シリアライズに対応させるためのメンバ関数を定義する
	template <class Archive>
	void SIV3D_SERIALIZE(Archive& archive)
	{
		archive(name, id, score);
	}
};

void Main()
{
	{
		// 記録したいデータ
		const Array<GameScore> scores =
		{
			{ U"Player1", 111, 1000 },
			{ U"Player2", 222, 2000 },
			{ U"Player3", 333, 3000 },
		};

		// バイナリファイルをオープン
		Serializer<BinaryWriter> writer{ U"score.bin" };

		if (!writer) // もしオープンに失敗したら
		{
			throw Error{ U"Failed to open `score.bin`" };
		}

		// シリアライズに対応したデータを記録
		writer(scores);

		// writer のデストラクタで自動的にファイルがクローズされる
	}

	// 読み込み先のデータ
	Array<GameScore> scores;
	{
		// バイナリファイルをオープン
		Deserializer<BinaryReader> reader{ U"score.bin" };

		if (!reader) // もしオープンに失敗したら
		{
			throw Error{ U"Failed to open `score.bin`" };
		}

		// バイナリファイルからシリアライズ対応型のデータを読み込む
		// (自動でリサイズが行われる)
		reader(scores);

		// reader のデストラクタで自動的にファイルがクローズされる
	}

	// 読み込んだスコアを確認
	for (const auto& score : scores)
	{
		Print << U"{}(id: {}): {}"_fmt(score.name, score.id, score.score);
	}

	while (System::Update())
	{

	}
}
Ryo SuzukiRyo Suzuki

Image::stamp()

  • Image::paint(): 書き込み先のアルファ値を更新しない
  • Image::stamp(): 書き込み先のアルファ値がソース画像のアルファ値より小さければ上書き
  • Image::overwrite(): 書き込み先のアルファ値を必ず上書き

Color{ 0, 0 } で完全に透過している Image に、スタンプを押すように別の画像を書き込めるように(.paint() ではアルファが更新されなかった)。

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

# include <Siv3D.hpp>

void Main()
{
    Image image{ 800, 600, Color{ 0, 0 } };
    DynamicTexture texture{ image };
    const Image emoji{ U"🐈"_emoji };

    while (System::Update())
    {
        if (MouseL.down())
        {
            emoji.stampAt(image, Cursor::Pos());
            texture.fill(image);
        }

        for (auto p : step({ 20, 15 }))
        {
            if (IsEven(p.x + p.y))
            {
                Rect{ p * 40, 40 }.draw(ColorF{ 0.5 });
            }
        }

        texture.draw();
    }
}
Ryo SuzukiRyo Suzuki

Microphone (v0.6 版)

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

void Main()
{
	if (System::EnumerateMicrophones().isEmpty())
	{
		throw Error{ U"No microphone is connected" };
	}

	const Microphone mic{ 5s, Loop::Yes, StartImmediately::Yes };

	if (!mic.isRecording())
	{
		throw Error{ U"Failed to start recording" };
	}

	LineString points(1600, Vec2{ 0, 300 });

	while (System::Update())
	{
		const Wave& wave = mic.getBuffer();
		const size_t writePos = mic.posSample();
		{
			constexpr size_t samples_show = 1600;
			const size_t headLength = Min(writePos, samples_show);
			const size_t tailLength = (samples_show - headLength);
			size_t pos = 0;

			for (size_t i = 0; i < tailLength; ++i)
			{
				const float a = wave[wave.size() - tailLength + i].left;
				points[pos].set(pos * 0.5, 300 + a * 280);
				++pos;
			}

			for (size_t i = 0; i < headLength; ++i)
			{
				const float a = wave[writePos - headLength + i].left;
				points[pos].set(pos * 0.5, 300 + a * 280);
				++pos;
			}
		}

		const double mean = mic.mean();
		const double rootMeanSquare = mic.rootMeanSquare();
		const double peak = mic.peak();

		points.draw();

		Line{ 0, (300 - mean * 280), Arg::direction(800, 0) }.draw(HSV{ 200 });
		Line{ 0, (300 - rootMeanSquare * 280), Arg::direction(800, 0) }.draw(HSV{ 120 });
		Line{ 0, (300 - peak * 280), Arg::direction(800, 0) }.draw(HSV{ 40 });
	}
}
Ryo SuzukiRyo Suzuki

Microphone FFT (v0.6 版)

# include <Siv3D.hpp>

void Main()
{
	if (System::EnumerateMicrophones().isEmpty())
	{
		throw Error{ U"No microphone is connected" };
	}

	const Microphone mic{ StartImmediately::Yes };

	if (!mic.isRecording())
	{
		throw Error{ U"Failed to start recording" };
	}

	FFTResult fft;

	while (System::Update())
	{
		// FFT の結果を取得
		mic.fft(fft);

		// 結果を可視化
		for (auto i : step(800))
		{
			const double size = Pow(fft.buffer[i], 0.6f) * 1200;
			RectF{ Arg::bottomLeft(i, 600), 1, size }.draw(HSV{ 240 - i });
		}

		// 周波数表示
		Rect{ Cursor::Pos().x, 0, 1, Scene::Height() }.draw();
		ClearPrint();
		Print << U"{} Hz"_fmt(Cursor::Pos().x * fft.resolution);
	}
}
Ryo SuzukiRyo Suzuki

Audio の強化

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

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

void Main()
{
	Window::Resize(1280, 720);

	Audio audio;
	double posSec = 0.0;
	double volume = 1.0;
	double pan = 0.0;
	double speed = 1.0;
	bool loop = false;

	Array<float> busSamples;
	Array<float> globalSamples;
	FFTResult busFFT;
	FFTResult globalFFT;
	LineString lines(256, Vec2{ 0, 0 });

	bool pitch = false;
	double pitchShift = 0.0;

	bool lpf = false;
	double lpfCutoffFrequency = 800.0;
	double lpfResonance = 0.5;
	double lpfWet = 1.0;

	bool hpf = false;
	double hpfCutoffFrequency = 800.0;
	double hpfResonance = 0.5;
	double hpfWet = 1.0;

	bool echo = false;
	double delay = 0.1;
	double decay = 0.5;
	double echoWet = 0.5;

	bool reverb = false;
	bool freeze = false;
	double roomSize = 0.5;
	double damp = 0.0;
	double width = 0.5;
	double reverbWet = 0.5;

	while (System::Update())
	{
		ClearPrint();
		Print << U"GlobalAudio::GetActiveVoiceCount(): " << GlobalAudio::GetActiveVoiceCount();
		Print << U"isEmpty : " << audio.isEmpty();
		Print << U"isStreaming : " << audio.isStreaming();
		Print << U"sampleRate : " << audio.sampleRate();
		Print << U"samples : " << audio.samples();
		Print << U"lengthSec : " << audio.lengthSec();
		Print << U"posSample : " << audio.posSample();
		Print << U"posSec : " << (posSec = audio.posSec());
		Print << U"isActive : " << audio.isActive();
		Print << U"isPlaying : " << audio.isPlaying();
		Print << U"isPaused : " << audio.isPaused();
		Print << U"samplesPlayed : " << audio.samplesPlayed();
		Print << U"isLoop : " << (loop = audio.isLoop());
		Print << U"getLoopTimingtLoop : " << audio.getLoopTiming().beginPos << U", " << audio.getLoopTiming().endPos;
		Print << U"loopCount : " << audio.loopCount();
		Print << U"getVolume : " << (volume = audio.getVolume());
		Print << U"getPan : " << (pan = audio.getPan());
		Print << U"getSpeed : " << (speed = audio.getSpeed());

		if (SimpleGUI::Button(U"Open audio file", Vec2{ 60, 560 }))
		{
			audio.stop(0.5s);
			audio = Dialog::OpenAudio(Audio::Stream);
		}

		{
			GlobalAudio::BusGetSamples(0, busSamples);
			GlobalAudio::BusGetFFT(0, busFFT);

			for (auto [i, s] : Indexed(busSamples))
			{
				lines[i].set((300.0 + i), (200.0 - s * 100.0));
			}

			if (busSamples)
			{
				lines.draw(2, Palette::Orange);
			}

			for (auto [i, s] : Indexed(busFFT.buffer))
			{
				RectF{ Arg::bottomLeft(300 + i, 300), 1, (s * 4) }.draw();
			}
		}

		{
			GlobalAudio::GetSamples(globalSamples);
			GlobalAudio::GetFFT(globalFFT);

			for (auto [i, s] : Indexed(globalSamples))
			{
				lines[i].set((300.0 + i), (550.0 - s * 100.0));
			}

			if (globalSamples)
			{
				lines.draw(2, Palette::Orange);
			}

			for (auto [i, s] : Indexed(globalFFT.buffer))
			{
				RectF{ Arg::bottomLeft(300 + i, 650), 1, (s * 4) }.draw();
			}
		}

		if (SimpleGUI::Button(U"Play", Vec2{ 600, 20 }, 80, !audio.isPlaying()))
		{
			audio.play();
		}

		if (SimpleGUI::Button(U"Pause", Vec2{ 690, 20 }, 80, (audio.isPlaying() && !audio.isPaused())))
		{
			audio.pause();
		}

		if (SimpleGUI::Button(U"Stop", Vec2{ 780, 20 }, 80, (audio.isPlaying() || audio.isPaused())))
		{
			audio.stop();
		}

		if (SimpleGUI::Button(U"Play in 2s", Vec2{ 870, 20 }, 120, !audio.isPlaying()))
		{
			audio.play(2s);
		}

		if (SimpleGUI::Button(U"Pause in 2s", Vec2{ 1000, 20 }, 120, (audio.isPlaying() && !audio.isPaused())))
		{
			audio.pause(2s);
		}

		if (SimpleGUI::Button(U"Stop in 2s", Vec2{ 1130, 20 }, 120, (audio.isPlaying() || audio.isPaused())))
		{
			audio.stop(2s);
		}

		if (SimpleGUI::Slider(U"{:.1f} / {:.1f}"_fmt(posSec, audio.lengthSec()), posSec, 0.0, audio.lengthSec(), Vec2{ 600, 60 }, 160, 360))
		{
			if (MouseL.down() || !Cursor::DeltaF().isZero()) // シークの連続(ノイズの原因)を防ぐ
			{
				audio.seekTime(posSec);
			}
		}

		if (SimpleGUI::CheckBox(loop, U"Loop", Vec2{ 1130, 60 }))
		{
			audio.setLoop(loop);
		}

		if (SimpleGUI::Slider(U"volume: {:.2f}"_fmt(volume), volume, Vec2{ 600, 110 }, 140, 130))
		{
			audio.setVolume(volume);
		}

		if (SimpleGUI::Button(U"0.0 in 2s", Vec2{ 880, 110 }, 110, audio.isActive()))
		{
			audio.fadeVolume(0.0, 2s);
		}

		if (SimpleGUI::Button(U"0.5 in 2s", Vec2{ 1000, 110 }, 110, audio.isActive()))
		{
			audio.fadeVolume(0.5, 2s);
		}

		if (SimpleGUI::Button(U"1.0 in 2s", Vec2{ 1120, 110 }, 110, audio.isActive()))
		{
			audio.fadeVolume(1.0, 2s);
		}

		if (SimpleGUI::Slider(U"pan: {:.2f}"_fmt(pan), pan, -1.0, 1.0, Vec2{ 600, 150 }, 140, 130))
		{
			audio.setPan(pan);
		}

		if (SimpleGUI::Button(U"-1.0 in 2s", Vec2{ 880, 150 }, 110, audio.isActive()))
		{
			audio.fadePan(-1.0, 2s);
		}

		if (SimpleGUI::Button(U"0.0 in 2s", Vec2{ 1000, 150 }, 110, audio.isActive()))
		{
			audio.fadePan(0.0, 2s);
		}

		if (SimpleGUI::Button(U"1.0 in 2s", Vec2{ 1120, 150 }, 110, audio.isActive()))
		{
			audio.fadePan(1.0, 2s);
		}

		if (SimpleGUI::Slider(U"speed: {:.3f}"_fmt(speed), speed, 0.0, 4.0, Vec2{ 600, 190 }, 140, 130))
		{
			audio.setSpeed(speed);
		}

		if (SimpleGUI::Button(U"0.8 in 2s", Vec2{ 880, 190 }, 110, audio.isActive()))
		{
			audio.fadeSpeed(0.8, 2s);
		}

		if (SimpleGUI::Button(U"1.0 in 2s", Vec2{ 1000, 190 }, 110, audio.isActive()))
		{
			audio.fadeSpeed(1.0, 2s);
		}

		if (SimpleGUI::Button(U"1.2 in 2s", Vec2{ 1120, 190 }, 110, audio.isActive()))
		{
			audio.fadeSpeed(1.2, 2s);
		}

		bool updatePitch = false;
		bool updateLPF = false;
		bool updateHPF = false;
		bool updateEcho = false;
		bool updateReverb = false;

		if (SimpleGUI::CheckBox(pitch, U"Pitch", Vec2{ 600, 240 }, 120, GlobalAudio::SupportsPitchShift()))
		{
			if (pitch)
			{
				updatePitch = true;
			}
			else
			{
				GlobalAudio::BusClearFilter(0, 0);
			}
		}
		updatePitch |= SimpleGUI::Slider(U"pitchShift: {:.2f}"_fmt(pitchShift), pitchShift, -12.0, 12.0, Vec2{ 720, 240 }, 160, 300);

		if (SimpleGUI::CheckBox(lpf, U"LPF", Vec2{ 600, 280 }, 120))
		{
			if (lpf)
			{
				updateLPF = true;
			}
			else
			{
				GlobalAudio::BusClearFilter(0, 1);
			}
		}
		updateLPF |= SimpleGUI::Slider(U"cutoffFrequency: {:.0f}"_fmt(lpfCutoffFrequency), lpfCutoffFrequency, 10, 4000, Vec2{ 720, 280 }, 220, 240);
		updateLPF |= SimpleGUI::Slider(U"resonance: {:.2f}"_fmt(lpfResonance), lpfResonance, 0.1, 8.0, Vec2{ 720, 310 }, 220, 240);
		updateLPF |= SimpleGUI::Slider(U"wet: {:.2f}"_fmt(lpfWet), lpfWet, Vec2{ 720, 340 }, 220, 240);

		if (SimpleGUI::CheckBox(hpf, U"HPF", Vec2{ 600, 380 }, 120))
		{
			if (hpf)
			{
				updateHPF = true;
			}
			else
			{
				GlobalAudio::BusClearFilter(0, 2);
			}
		}
		updateHPF |= SimpleGUI::Slider(U"cutoffFrequency: {:.0f}"_fmt(hpfCutoffFrequency), hpfCutoffFrequency, 10, 4000, Vec2{ 720, 380 }, 220, 240);
		updateHPF |= SimpleGUI::Slider(U"resonance: {:.2f}"_fmt(hpfResonance), hpfResonance, 0.1, 8.0, Vec2{ 720, 410 }, 220, 240);
		updateHPF |= SimpleGUI::Slider(U"wet: {:.2f}"_fmt(hpfWet), hpfWet, Vec2{ 720, 440 }, 220, 240);

		if (SimpleGUI::CheckBox(echo, U"Echo", Vec2{ 600, 480 }, 120))
		{
			if (echo)
			{
				updateEcho = true;
			}
			else
			{
				GlobalAudio::BusClearFilter(0, 3);
			}
		}
		updateEcho |= SimpleGUI::Slider(U"delay: {:.2f}"_fmt(delay), delay, Vec2{ 720, 480 }, 220, 240);
		updateEcho |= SimpleGUI::Slider(U"decay: {:.2f}"_fmt(decay), decay, Vec2{ 720, 510 }, 220, 240);
		updateEcho |= SimpleGUI::Slider(U"wet: {:.2f}"_fmt(echoWet), echoWet, Vec2{ 720, 540 }, 220, 240);

		if (SimpleGUI::CheckBox(reverb, U"Reverb", Vec2{ 600, 580 }, 120))
		{
			if (reverb)
			{
				updateReverb = true;
			}
			else
			{
				GlobalAudio::BusClearFilter(0, 4);
			}
		}
		updateReverb |= SimpleGUI::CheckBox(freeze, U"freeze", Vec2{ 720, 580 }, 110);
		updateReverb |= SimpleGUI::Slider(U"roomSize: {:.2f}"_fmt(roomSize), roomSize, 0.001, 1.0, { 830, 580 }, 150, 200);
		updateReverb |= SimpleGUI::Slider(U"damp: {:.2f}"_fmt(damp), damp, Vec2{ 720, 610 }, 220, 240);
		updateReverb |= SimpleGUI::Slider(U"width: {:.2f}"_fmt(width), width, Vec2{ 720, 640 }, 220, 240);
		updateReverb |= SimpleGUI::Slider(U"wet: {:.2f}"_fmt(reverbWet), reverbWet, Vec2{ 720, 670 }, 220, 240);

		if (pitch && updatePitch)
		{
			GlobalAudio::BusSetPitchShiftFilter(0, 0, pitchShift);
		}

		if (lpf && updateLPF)
		{
			GlobalAudio::BusSetLowPassFilter(0, 1, lpfCutoffFrequency, lpfResonance, lpfWet);
		}

		if (hpf && updateHPF)
		{
			GlobalAudio::BusSetHighPassFilter(0, 2, hpfCutoffFrequency, hpfResonance, hpfWet);
		}

		if (echo && updateEcho)
		{
			GlobalAudio::BusSetEchoFilter(0, 3, delay, decay, echoWet);
		}

		if (reverb && updateReverb)
		{
			GlobalAudio::BusSetReverbFilter(0, 4, freeze, roomSize, damp, width, reverbWet);
		}
	}

	if (GlobalAudio::GetActiveVoiceCount())
	{
		GlobalAudio::FadeVolume(0.0, 0.5s);
		System::Sleep(0.5s);
	}
}
Ryo SuzukiRyo Suzuki

音楽プレイヤー (v0.6 版)

# include <Siv3D.hpp>

void Main()
{
	// 音楽
	Audio audio;

	// FFT の結果
	FFTResult fft;

	// 再生位置の変更の有無
	bool seeking = false;

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

		// 再生・演奏時間
		const String time = FormatTime(SecondsF{ audio.posSec() }, U"M:ss")
			+ U'/' + FormatTime(SecondsF{ audio.lengthSec() }, U"M:ss");

		// プログレスバーの進み具合
		double progress = static_cast<double>(audio.posSample()) / audio.samples();

		if (audio.isPlaying())
		{
			// FFT 解析
			FFT::Analyze(fft, audio);

			// 結果を可視化
			for (auto i : step(Min(Scene::Width(), static_cast<int32>(fft.buffer.size()))))
			{
				const double size = Pow(fft.buffer[i], 0.6f) * 1000;
				RectF{ Arg::bottomLeft(i, 480), 1, size }.draw(HSV{ 240.0 - i });
			}

			// 周波数表示
			Rect{ Cursor::Pos().x, 0, 1, Scene::Height() }.draw();
			Print << U"{:.2f} Hz"_fmt(Cursor::Pos().x * fft.resolution);
		}

		// 再生
		if (SimpleGUI::Button(U"Play", Vec2{ 40, 500 }, 120, audio && !audio.isPlaying()))
		{
			audio.play(0.2s);
		}

		// 一時停止
		if (SimpleGUI::Button(U"Pause", Vec2{ 170, 500 }, 120, audio.isPlaying()))
		{
			audio.pause(0.2s);
		}

		// フォルダから音楽ファイルを開く
		if (SimpleGUI::Button(U"Open", Vec2{ 300, 500 }, 120))
		{
			audio.stop(0.5s);
			audio = Dialog::OpenAudio();
			audio.play();
		}

		// スライダー
		if (SimpleGUI::Slider(time, progress, Vec2{ 40, 540 }, 130, 590, !audio.isEmpty()))
		{
			audio.pause(0.05s);

			while (audio.isPlaying()) // 再生が止まるまで待機
			{
				System::Sleep(0.01s);
			}

			// 再生位置を変更
			audio.seekSamples(static_cast<size_t>(audio.samples() * progress));

			// ノイズを避けるため、スライダーから手を離すまで再生は再開しない
			seeking = true;
		}
		else if (seeking && MouseL.up())
		{
			// 再生を再開
			audio.play(0.05s);
			seeking = false;
		}
	}

	// 終了時に再生中の場合、音量をフェードアウト
	if (audio.isPlaying())
	{
		audio.fadeVolume(0.0, 0.3s);
		System::Sleep(0.3s);
	}
}
Ryo SuzukiRyo Suzuki

Line::drawDoubleHeadedArrow()

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

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

void Main()
{
	Scene::SetBackground(ColorF{ 0.7, 0.9, 0.8 });

	while (System::Update())
	{
		for (auto i : step(5))
		{
			Line{ 100, (100 + i * 100), Arg::direction(100 + i * 100, 0) }
				.drawDoubleHeadedArrow(5 + i * 3, { 10 + i * 10, 10 + i * 10 }, ColorF{ 0.25 });
		}

		Line{ 300, 100, 700, 500 }
			.drawDoubleHeadedArrow(10, { 20, 60 }, ColorF{ 0.25 });

		Line{ 400, 100, 700, 400 }
			.drawDoubleHeadedArrow(5, { 30, 30 }, ColorF{ 0.25 });
	}
}
Ryo SuzukiRyo Suzuki

ScreenCapture::SetShortcutKeys

  • スクリーンショット保存のショートカットキーをカスタマイズできるように
  • OpenSiv3D v0.6 のデフォルトは { KeyPrintScreen, KeyF12 }
#include <Siv3D.hpp> // OpenSiv3D v0.6.0

void Main()
{
	// PrintScreen または (Ctrl + P) キーでスクリーンショットを保存
	ScreenCapture::SetShortcutKeys({ KeyPrintScreen, (KeyControl + KeyP) });

	const Font font{ 40 };

	while (System::Update())
	{
		font(Scene::FrameCount()).drawAt(Scene::Center());
	}
}
Ryo SuzukiRyo Suzuki

TextStyle

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

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

void Main()
{
	Window::Resize(1280, 720);

	// 大きいと拡大描画時にきれいになるが、フォントの生成時間・メモリ消費が増える
	constexpr int32 fontSize = 70;

	// このサイズだけ、文字の周囲に輪郭や影のエフェクトを付加できる。フォントの生成時間・メモリ消費が増える
	constexpr int32 bufferThickness = 5;

	// ビットマップフォントでは輪郭や影のエフェクトの利用は不可
	const Font fontBitmap{ FontMethod::Bitmap, fontSize, U"example/font/RocknRoll/RocknRollOne-Regular.ttf" };

	const Font fontSDF{ FontMethod::SDF, fontSize, U"example/font/RocknRoll/RocknRollOne-Regular.ttf" };
	fontSDF.setBufferThickness(bufferThickness);

	const Font fontMSDF{ FontMethod::MSDF, fontSize, U"example/font/RocknRoll/RocknRollOne-Regular.ttf" };
	fontMSDF.setBufferThickness(bufferThickness);

	bool outline = false;
	bool shadow = false;
	double inner = 0.1, outer = 0.1;
	Vec2 shadowOffset{ 2.0, 2.0 };
	ColorF textColor{ 1.0 };
	ColorF outlineColor{ 0.0 };
	ColorF shadowColor{ 0.0, 0.5 };
	HSV background = ColorF{ 0.8 };

	Camera2D camera{ Scene::Center(), 1.0 };

	while (System::Update())
	{
		Scene::SetBackground(background);

		TextStyle textStyle;
		{
			if (outline && shadow)
			{
				textStyle = TextStyle::OutlineShadow(inner, outer, outlineColor, shadowOffset, shadowColor);
			}
			else if (outline)
			{
				textStyle = TextStyle::Outline(inner, outer, outlineColor);
			}
			else if (shadow)
			{
				textStyle = TextStyle::Shadow(shadowOffset, shadowColor);
			}
		}

		camera.update();
		{
			auto t = camera.createTransformer();
			fontBitmap(U"Siv3D, 渋三次元 (Bitmap)").draw(Vec2{ 100, 250 }, textColor);
			fontSDF(U"Siv3D, 渋三次元 (SDF)").draw(textStyle, Vec2{ 100, 330 }, textColor);
			fontMSDF(U"Siv3D, 渋三次元 (MSDF)").draw(textStyle, Vec2{ 100, 410 }, textColor);
		}

		SimpleGUI::CheckBox(outline, U"Outline", Vec2{ 20, 20 }, 130);
		SimpleGUI::Slider(U"Inner: {:.2f}"_fmt(inner), inner, -0.5, 0.5, Vec2{ 160, 20 }, 120, 120, outline);
		SimpleGUI::Slider(U"Outer: {:.2f}"_fmt(outer), outer, -0.5, 0.5, Vec2{ 160, 60 }, 120, 120, outline);

		SimpleGUI::CheckBox(shadow, U"Shadow", Vec2{ 20, 100 }, 130);
		SimpleGUI::Slider(U"offsetX: {:.1f}"_fmt(shadowOffset.x), shadowOffset.x, -5.0, 5.0, Vec2{ 160, 100 }, 120, 120, shadow);
		SimpleGUI::Slider(U"offsetY: {:.1f}"_fmt(shadowOffset.y), shadowOffset.y, -5.0, 5.0, Vec2{ 160, 140 }, 120, 120, shadow);

		SimpleGUI::Headline(U"Text", Vec2{ 420, 20 });
		SimpleGUI::Slider(U"R", textColor.r, Vec2{ 420, 60 }, 20, 80);
		SimpleGUI::Slider(U"G", textColor.g, Vec2{ 420, 100 }, 20, 80);
		SimpleGUI::Slider(U"B", textColor.b, Vec2{ 420, 140 }, 20, 80);
		SimpleGUI::Slider(U"A", textColor.a, Vec2{ 420, 180 }, 20, 80);

		SimpleGUI::Headline(U"Outline", Vec2{ 540, 20 });
		SimpleGUI::Slider(U"R", outlineColor.r, Vec2{ 540, 60 }, 20, 80, outline);
		SimpleGUI::Slider(U"G", outlineColor.g, Vec2{ 540, 100 }, 20, 80, outline);
		SimpleGUI::Slider(U"B", outlineColor.b, Vec2{ 540, 140 }, 20, 80, outline);
		SimpleGUI::Slider(U"A", outlineColor.a, Vec2{ 540, 180 }, 20, 80, outline);

		SimpleGUI::Headline(U"Shadow", Vec2{ 660, 20 });
		SimpleGUI::Slider(U"R", shadowColor.r, Vec2{ 660, 60 }, 20, 80, shadow);
		SimpleGUI::Slider(U"G", shadowColor.g, Vec2{ 660, 100 }, 20, 80, shadow);
		SimpleGUI::Slider(U"B", shadowColor.b, Vec2{ 660, 140 }, 20, 80, shadow);
		SimpleGUI::Slider(U"A", shadowColor.a, Vec2{ 660, 180 }, 20, 80, shadow);

		SimpleGUI::ColorPicker(background, Vec2{ 780, 20 });
	}
}
Ryo SuzukiRyo Suzuki

SimpleHTTP

ファイルダウンロードの基本サンプル

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

void Main()
{
	const URL url = U"https://raw.githubusercontent.com/Siv3D/siv3d.docs.images/master/logo/logo.png";
	const FilePath saveFilePath = U"logo.png";
	Texture texture;

	if (SimpleHTTP::Save(url, saveFilePath).isOK())
	{
		texture = Texture{ saveFilePath };
	}
	else
	{
		Print << U"Failed.";
	}

	while (System::Update())
	{
		if (texture)
		{
			texture.draw();
		}
	}
}

ファイルダウンロードのサンプル

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

void Main()
{
	const URL url = U"https://raw.githubusercontent.com/Siv3D/siv3d.docs.images/master/logo/logo.png";
	const FilePath saveFilePath = DateTime::Now().format(U"mmss\'.png\'");

	if (const auto response = SimpleHTTP::Save(url, saveFilePath))
	{
		Console << U"------";
		Console << response.getStatusLine().rtrimmed();
		Console << U"status code: " << FromEnum(response.getStatusCode());
		Console << U"------";
		Console << response.getHeader().rtrimmed();
		Console << U"------";
	}
	else
	{
		Print << U"Failed.";
	}

	Print << saveFilePath;
	const Texture texture{ saveFilePath };

	while (System::Update())
	{
		texture.draw();
	}
}

ファイル非同期ダウンロードの基本サンプル

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

void Main()
{
	const URL url = U"https://raw.githubusercontent.com/Siv3D/siv3d.docs.images/master/logo/logo.png";
	const FilePath saveFilePath = U"logo2.png";
	AsyncHTTPTask task = SimpleHTTP::SaveAsync(url, saveFilePath);
	Texture texture;

	while (System::Update())
	{
		if (task.isReady())
		{
			if (task.getResponse().isOK())
			{
				texture = Texture{ saveFilePath };
			}
			else
			{
				Print << U"Failed.";
			}
		}

		if (texture)
		{
			texture.draw();
		}
	}
}

ファイル非同期ダウンロードのサンプル(キャンセル・進捗表示)

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

void Main()
{
	const Font font{ 24, Typeface::Medium };
	const URL url = U"http://httpbin.org/drip?duration=4&numbytes=1024&code=200&delay=0";
	const FilePath saveFilePath = U"drip.txt";
	AsyncHTTPTask task;

	while (System::Update())
	{
		if (SimpleGUI::Button(U"Download", Vec2{ 20, 20 }, 140, task.isEmpty()))
		{
			task = SimpleHTTP::SaveAsync(url, saveFilePath);
		}

		if (SimpleGUI::Button(U"Cancel", Vec2{ 180, 20 }, 140, (task.getStatus() == HTTPAsyncStatus::Downloading)))
		{
			task.cancel();
		}

		const HTTPProgress progress = task.getProgress();

		if (progress.status == HTTPAsyncStatus::None_)
		{
			font(U"status: None_").draw(20, 60);
		}
		else if (progress.status == HTTPAsyncStatus::Downloading)
		{
			font(U"status: Downloading").draw(20, 60);
		}
		else if (progress.status == HTTPAsyncStatus::Failed)
		{
			font(U"status: Failed").draw(20, 60);
		}
		else if (progress.status == HTTPAsyncStatus::Canceled)
		{
			font(U"status: Canceled").draw(20, 60);
		}
		else if (progress.status == HTTPAsyncStatus::Succeeded)
		{
			font(U"status: Succeeded").draw(20, 60);
		}

		if (progress.status == HTTPAsyncStatus::Downloading)
		{
			const int64 downloaded = progress.downloaded_bytes;
			const Optional<int64> total = progress.download_total_bytes;

			if (total)
			{
				font(U"downloaded: {} bytes / {} bytes"_fmt(downloaded, *total)).draw(20, 100);

				const double progress0_1 = (static_cast<double>(downloaded) / *total);
				const RectF rect{ 20, 140, 500, 30 };
				rect.drawFrame(2, 0);
				RectF{ rect.pos, (rect.w * progress0_1), rect.h }.draw();
			}
			else
			{
				font(U"downloaded: {} bytes"_fmt(downloaded)).draw(20, 100);
			}
		}

		if (task.isReady())
		{
			if (const auto& response = task.getResponse())
			{
				Console << U"------";
				Console << response.getStatusLine().rtrimmed();
				Console << U"status code: " << FromEnum(response.getStatusCode());
				Console << U"------";
				Console << response.getHeader().rtrimmed();
				Console << U"------";

				if (response.isOK())
				{
					Console << FileSystem::FileSize(saveFilePath) << U" bytes";
					Console << U"------";
				}
			}
			else
			{
				Print << U"Failed.";
			}
		}
	}
}

GET リクエストのサンプル

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

void Main()
{
	const URL url = U"https://httpbin.org/bearer";
	const HashTable<String, String> headers = { { U"Authorization", U"Bearer TOKEN123456abcdef" } };
	const FilePath saveFilePath = U"auth_result.json";

	if (const auto response = SimpleHTTP::Get(url, headers, saveFilePath))
	{
		Console << U"------";
		Console << response.getStatusLine().rtrimmed();
		Console << U"status code: " << FromEnum(response.getStatusCode());
		Console << U"------";
		Console << response.getHeader().rtrimmed();
		Console << U"------";

		if (response.isOK())
		{
			Print << TextReader{ saveFilePath }.readAll();
		}
	}
	else
	{
		Print << U"Failed.";
	}

	while (System::Update())
	{

	}
}

POST リクエストのサンプル

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

void Main()
{
	const URL url = U"https://httpbin.org/post";
	const HashTable<String, String> headers = { { U"Content-Type", U"application/json" } };
	const std::string data = JSON
	{
		{ U"body", U"Hello, Siv3D!" },
		{ U"date", DateTime::Now().format() },
	}.formatUTF8();
	const FilePath saveFilePath = U"post_result.json";

	if (auto response = SimpleHTTP::Post(url, headers, data.data(), data.size(), saveFilePath))
	{
		Console << U"------";
		Console << response.getStatusLine().rtrimmed();
		Console << U"status code: " << FromEnum(response.getStatusCode());
		Console << U"------";
		Console << response.getHeader().rtrimmed();
		Console << U"------";

		if (response.isOK())
		{
			Print << TextReader{ saveFilePath }.readAll();
		}
	}
	else
	{
		Print << U"Failed.";
	}

	while (System::Update())
	{

	}
}
Ryo SuzukiRyo Suzuki

SimpleAnimation

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

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

void Main()
{
	Scene::SetBackground(ColorF{ 0.3, 0.5, 0.4 });
	const Texture texture1{ U"🐥"_emoji };
	const Texture texture2{ U"🐢"_emoji };

	SimpleAnimation a1;
	a1.setLoop(12s)
		.set(U"r", { 0.5s, 0 }, { 1.5s, 1 }, EaseOutBounce)
		.set(U"g", { 1s, 0 }, { 2s, 1 }, EaseOutBounce)
		.set(U"b", { 1.5s, 0 }, { 2.5s, 1 }, EaseOutBounce)
		.set(U"angle", { 3s, 0_deg }, { 8.5s, 720_deg }, EaseOutBounce)
		.set(U"size", { 0s, 0 }, { 0.5s, 320 }, EaseOutExpo)
		.set(U"size", { 9s, 320 }, { 9.5s, 0 }, EaseOutExpo)
		.start();

	SimpleAnimation a2;
	a2.setLoop(6s)
		.set(U"x", { 1s, 150 }, { 3s, 650 }, EaseInOutExpo)
		.set(U"y", { 0s, 350 }, { 1s, 150 }, EaseOutBack)
		.set(U"y", { 3s, 150 }, { 4s, 350 }, EaseInQuad)
		.set(U"t", { 0s, 0 }, { 4s, 12_pi }, EaseInOutQuad)
		.set(U"a", { 5s, 1 }, { 6s, 0 }, EaseOutCubic)
		.start();

	SimpleAnimation a3;
	a3.setLoop(6s)
		.set(U"x", { 0s, 100 }, { 3s, 700 }, EaseInOutQuad)
		.set(U"x", { 3s, 700 }, { 6s, 100 }, EaseInOutQuad)
		.set(U"mirrored", { 0s, 1 }, { 3s, 1 })
		.set(U"mirrored", { 3s, 0 }, { 6s, 0 })
		.start();

	while (System::Update())
	{
		Triangle{ Scene::Center(), a1[U"size"], a1[U"angle"] }
			.draw(ColorF{ a1[U"r"], 0, 0 }, ColorF{ 0, a1[U"g"], 0 }, ColorF{ 0, 0, a1[U"b"] });

		texture1
			.drawAt(a2[U"x"], a2[U"y"] + Math::Sin(a2[U"t"]) * 20.0, ColorF{ 1, a2[U"a"] });

		texture2
			.mirrored(a3[U"mirrored"])
			.drawAt(a3[U"x"], 500);
	}
}
Ryo SuzukiRyo Suzuki

TimeProfiler (v0.6 版)

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

void Main()
{
	TimeProfiler profiler{ U"Test" };

	while (System::Update())
	{
		ClearPrint();
		profiler.print();

		{
			profiler.begin(U"Rect");

			for (auto i : step(20))
			{
				Rect{ Arg::center(20 + i * 40, 200), 30 }.draw();
			}

			profiler.end(U"Rect");
		}

		{
			profiler.begin(U"Circle");

			for (auto i : step(20))
			{
				Circle{ 20 + i * 40, 300, 15 }.draw();
			}

			profiler.end(U"Circle");
		}

		{
			profiler.begin(U"Star");

			for (auto i : step(20))
			{
				Shape2D::Star(15, Vec2{ 20 + i * 40, 400 }).draw();
			}

			profiler.end(U"Star");
		}
	}

	profiler.log();
}
Ryo SuzukiRyo Suzuki

ペンタブレット入力

  • Windows 版のみ
# include <Siv3D.hpp> // OpenSiv3D v0.6

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

	Image image{ Scene::Size(), Palette::White };
	DynamicTexture texture{ image };

	while (System::Update())
	{
		ClearPrint();
		Print << U"IsAvailable: " << Pentablet::IsAvailable();
		Print << U"SupportsPressure: " << Pentablet::SupportsPressure();
		Print << U"SupportsTangentPressure: " << Pentablet::SupportsTangentPressure();
		Print << U"SupportsOrientation: " << Pentablet::SupportsOrientation();
		Print << U"Pressure: " << Pentablet::Pressure();
		Print << U"TangentPressure: " << Pentablet::TangentPressure();
		Print << U"Azimuth: " << Pentablet::Azimuth();
		Print << U"Altitude: " << Pentablet::Altitude();
		Print << U"Twist: " << Pentablet::Twist();

		if (MouseL.pressed())
		{
			const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
			const Point to = Cursor::Pos();

			const double thickness = (Math::Square(Pentablet::Pressure()) * 40.0);
			Line{ from, to }.overwrite(image, static_cast<int32>(thickness), ColorF{ 0.1 });

			texture.fill(image);
		}

		texture.draw();
	}
}
Ryo SuzukiRyo Suzuki

FontAsset (v0.6 版)

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

void Main()
{
	const String preloadText = U"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

	FontAsset::Register(U"MyFont", FontMethod::MSDF, 64, Typeface::Bold);

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

		for (auto [name, info] : FontAsset::Enumerate())
		{
			Print << name << U" | " <<
				((info.state == AssetState::Uninitialized) ? U"Uninitialized"
				: (info.state == AssetState::AsyncLoading) ? U"AsyncLoading"
				: (info.state == AssetState::Loaded) ? U"Loaded" : U"Failed")
				<< U" | tags = " << info.tags;
		}

		if (FontAsset::IsReady(U"MyFont"))
		{
			FontAsset(U"MyFont")(U"OpenSiv3D").draw(TextStyle::Outline(0.2, Palette::Red), 100, 20, 50);
		}

		if (SimpleGUI::Button(U"Release", Vec2{ 300, 20 }, 120))
		{
			FontAsset::Release(U"MyFont");
		}

		if (SimpleGUI::Button(U"Load", Vec2{ 440, 20 }, 120))
		{
			FontAsset::Load(U"MyFont", preloadText);
		}

		if (SimpleGUI::Button(U"LoadAsync", Vec2{ 580, 20 }, 120))
		{
			FontAsset::LoadAsync(U"MyFont", preloadText);
		}

		Circle{ Scene::Center(), 80 }
			.drawArc(Scene::Time() * 180_deg, 240_deg, 15);
	}
}
Ryo SuzukiRyo Suzuki

ViewFrustum

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

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

void DrawScene(const ViewFrustum& frustum, const Texture& groundTexture)
{
	Plane{ 60 }.draw(groundTexture);
	const ColorF orange = ColorF{ Palette::Orange }.removeSRGBCurve();

	for (auto z : Range(-5, 5))
	{
		for (auto x : Range(-8, 8))
		{
			for (auto y : Range(0, 8))
			{
				const Sphere s{ x,y,z, 0.2 };
				s.draw(frustum.intersects(s) ? orange : ColorF{ 1.0 });
			}
		}
	}
}

void Main()
{
	Window::Resize(1280, 720);
	Graphics3D::SetGlobalAmbientColor(ColorF{ 0.5 });

	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const Texture groundTexture{ U"example/texture/uv.png", TextureDesc::MippedSRGB };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	const MSRenderTexture renderTexture2{ 360, 240, TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera1{ renderTexture.size(), 25_deg, Vec3{ 10, 16, -32 } };
	BasicCamera3D camera2{ renderTexture2.size(), 25_deg };

	while (System::Update())
	{
		camera1.update(4.0);
		const Vec3 camera2Pos = Cylindrical{ 18, (Scene::Time() * 10_deg), (2 + Periodic::Sine0_1(4s) * 10) };
		camera2.setView(camera2Pos, Vec3::Zero());
		const Vec3 upDirection = Vec3{ Math::Sin(Scene::Time() * 0.2), 1, 0 }.normalized() * camera2.getLookAtOrientation();
		camera2.setUpDirection(upDirection);
		const ViewFrustum frustum{ camera2, 1.0, 50.0 };

		// 俯瞰カメラ (camera1)
		{
			Graphics3D::SetCameraTransform(camera1);
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			DrawScene(frustum, groundTexture);
			frustum.drawFrame(Palette::Red);
			Line3D{ camera2Pos, camera2Pos + upDirection * 4 }.draw(Palette::Red);
			OrientedBox{ frustum.getOrigin(), {1.2, 0.8, 2}, frustum.getOrientation() }.draw(ColorF{ 0.2 }.removeSRGBCurve());
		}

		// (camera2)
		{
			Graphics3D::SetCameraTransform(camera2);
			const ScopedRenderTarget3D target{ renderTexture2.clear(backgroundColor) };
			DrawScene(frustum, groundTexture);
		}

		// RenderTexture を 2D シーンに描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			renderTexture2.resolve();
			Shader::LinearToScreen(renderTexture);
			RectF{ 360,240 }.drawShadow({ 0,0 }, 8, 3);
			Shader::LinearToScreen(renderTexture2, Vec2{ 0,0 });
		}
	}
}
Ryo SuzukiRyo Suzuki

3D Ray 交差判定

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

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

template <class Shape>
void Draw(const Shape& shape, const Ray& ray)
{
	if (auto pos = ray.intersectsAt(shape))
	{
		shape.draw(Linear::Palette::Orange);
		Sphere{ *pos, 0.15 }.draw(Linear::Palette::Red);
	}
	else
	{
		shape.draw(HSV{ (shape.center.x * 60), 0.1, 0.95 }.removeSRGBCurve());
	}
}

void Main()
{
	Window::Resize(1280, 720);
	Graphics3D::SetGlobalAmbientColor(ColorF{ 0.25 });
	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 };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	const Sphere sphere = Sphere{ { 2, 2, 8 }, 2 };
	const Box box1 = Box::FromPoints({ -13, 0, 14 }, { -2, 4, 13 });
	const Box box2 = Box::FromPoints({ -14, 0, 14 }, { -13, 2, 2 });
	const Cylinder cy1{ {-8,2,4},{-4, 5,6}, 1 };
	const Cylinder cy2{ {-10,2,1},{-6, 3,-2}, 0.5 };

	while (System::Update())
	{
		camera.update(2.0);

		// 3D
		{
			Graphics3D::SetCameraTransform(camera);
			const Ray ray = camera.screenToRay(Cursor::PosF());
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			const OrientedBox ob1 = OrientedBox{ Arg::bottomCenter(-4, 0, 3), { 4, 2, 0.25 },  Quaternion::RotateY(Scene::Time() * -20_deg) };
			const OrientedBox ob2 = OrientedBox{ Arg::bottomCenter(0, 0, -1), { 4, 2, 0.25 },  Quaternion::RotateY(Scene::Time() * 20_deg) };
			const Cone cone{ { 4, 4, 0},{ 4, 7, 0}, 1.0, Quaternion::RotateZ(Scene::Time() * 10_deg) };

			Plane{ 64 }.draw(uvChecker);
			Draw(sphere, ray);
			Draw(box1, ray);
			Draw(box2, ray);
			Draw(cy1, ray);
			Draw(cy2, ray);
			Draw(ob1, ray);
			Draw(ob2, ray);
			Draw(cone, ray);
		}

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

Terrain

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

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

void Main()
{
	Window::Resize(1280, 720);

	const VertexShader vsTerrain = HLSL{ U"example/shader/hlsl/terrain_forward.hlsl", U"VS" }
		| GLSL{ U"example/shader/glsl/terrain_forward.vert", {{ U"VSPerView", 1 }, { U"VSPerObject", 2 }} };

	const PixelShader psTerrain = HLSL{ U"example/shader/hlsl/terrain_forward.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/terrain_forward.frag", {{ U"PSPerFrame", 0 }, { U"PSPerView", 1 }, { U"PSPerMaterial", 3 }} };

	const PixelShader psNormal = HLSL{ U"example/shader/hlsl/terrain_normal.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/terrain_normal.frag", {{U"PSConstants2D", 0}} };

	if ((not vsTerrain) || (not psTerrain) || (not psNormal))
	{
		return;
	}

	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const Texture terrainTexture{ U"example/texture/grass.jpg", TextureDesc::MippedSRGB };
	const Texture rockTexture{ U"example/texture/rock.jpg", TextureDesc::MippedSRGB };
	const Texture brushTexture{ U"example/particle.png" };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	const Mesh gridMesh{ MeshData::Grid({128, 128}, 128, 128) };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };
	RenderTexture heightmap{ Size{ 256, 256 }, ColorF{0.0}, TextureFormat::R32_Float };
	RenderTexture normalmap{ Size{ 256, 256 }, ColorF{0.0, 0.0, 0.0}, TextureFormat::R16G16_Float };

	while (System::Update())
	{
		camera.update(2.0);

		// 3D
		{
			Graphics3D::SetCameraTransform(camera);

			const ScopedCustomShader3D shader{ vsTerrain, psTerrain };
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			const ScopedRenderStates3D ss{ { ShaderStage::Vertex, 0, SamplerState::ClampLinear} };
			Graphics3D::SetVSTexture(0, heightmap);
			Graphics3D::SetPSTexture(1, normalmap);
			Graphics3D::SetPSTexture(2, rockTexture);

			gridMesh.draw(terrainTexture);
		}

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

		if (const bool gen = SimpleGUI::Button(U"Random", Vec2{270, 10});
			(gen || (MouseL | MouseR).pressed())) // 地形を編集
		{
			// heightmap を編集
			if (gen)
			{
				const PerlinNoiseF perlin{ RandomUint64() };
				Grid<float> grid(256, 256);
				for (auto p : step(grid.size()))
				{
					grid[p] = perlin.octave2D0_1(p / 256.0f, 5) * 16.0f;
				}
				const RenderTexture noise{ grid };
				const ScopedRenderTarget2D target{ heightmap };
				noise.draw();
			}
			else
			{
				const ScopedRenderTarget2D target{ heightmap };
				const ScopedRenderStates2D blend{ BlendState::Additive };
				brushTexture.scaled(1.0 + MouseL.pressed()).drawAt(Cursor::PosF(), ColorF{ Scene::DeltaTime() * 15.0 });
			}

			// normal map を更新
			{
				const ScopedRenderTarget2D target{ normalmap };
				const ScopedCustomShader2D shader{ psNormal };
				const ScopedRenderStates2D blend{ BlendState::Opaque, SamplerState::ClampLinear };
				heightmap.draw();
			}
		}

		heightmap.draw(ColorF{ 0.1 });
		normalmap.draw(0, 260);
	}
}
Ryo SuzukiRyo Suzuki

3D 基本

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

void Main()
{
	// ウインドウとシーンを 1280x720 にリサイズ
	Window::Resize(1280, 720);

	// 背景色 (リニアレンダリング用なので removeSRGBCurve() で SRGB カーブを除去)
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();

	// UV チェック用テクスチャ (ミップマップ使用。リニアレンダリング時に正しく扱われるよう、SRGB テクスチャであると明示)
	const Texture uvChecker{ U"example/texture/uv.png", TextureDesc::MippedSRGB };

	// 3D シーンを描く、マルチサンプリング対応レンダーテクスチャ
	// リニアレンダリングをするので TextureFormat::R8G8B8A8_Unorm_SRGB
	// 奥行きの比較のための深度バッファも使うので HasDepth::Yes
	// 内容を描画する前に、マルチサンプリングの解決のための resolve() が必要
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };

	// 3D シーンのデバッグ用カメラ
	// 縦方向の視野角 30°, カメラの位置 (10, 16, -32)
	// 前後移動: [W][S], 左右移動: [A][D], 上下移動: [E][X], 注視点移動: アローキー, 加速: [Shift][Ctrl]
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	while (System::Update())
	{
		// デバッグカメラの更新 (カメラの移動スピード: 2.0)
		camera.update(2.0);

		// 3D シーンにカメラを設定
		Graphics3D::SetCameraTransform(camera);

		// 3D 描画
		{
			// renderTexture を背景色で塗りつぶし、
			// renderTexture を 3D 描画のレンダーターゲットに
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

			// 床を描画
			Plane{ 64 }.draw(uvChecker);

			// ボックスを描画
			Box{ -8,2,0,4 }.draw(ColorF{ 0.8, 0.6, 0.4 }.removeSRGBCurve());

			// 球を描画
			Sphere{ 0,2,0,2 }.draw(ColorF{ 0.4, 0.8, 0.6 }.removeSRGBCurve());

			// 円柱を描画
			Cylinder{ 8, 2, 0, 2, 4 }.draw(ColorF{ 0.6, 0.4, 0.8 }.removeSRGBCurve());
		}

		// 3D シーンを 2D シーンに描画
		{
			// renderTexture を resolve する前に 3D 描画を実行する
			Graphics3D::Flush();

			// マルチサンプリングの解決
			renderTexture.resolve();

			// リニアレンダリングされた renderTexture をシーンに転送
			Shader::LinearToScreen(renderTexture);
		}
	}
}
Ryo SuzukiRyo Suzuki

ManagedScript (v0.6 版)

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

void Main()
{
	const ManagedScript script{ U"example/script/hello.as" };

	while (System::Update())
	{
		script.run();
	}
}